From e6e654badac5e08cc3baf9ca55e8c590769d2e03 Mon Sep 17 00:00:00 2001 From: Rahul Rajesh Date: Tue, 10 Feb 2026 14:19:38 -0500 Subject: [PATCH 1/9] platform: expand nmp object type flags to guint64 To allow for more than 32 NMP_OBJECT_* types. --- src/core/nm-ip-config.c | 8 ++++---- src/core/nm-ip-config.h | 2 +- src/core/nm-l3cfg.c | 4 ++-- src/core/nm-l3cfg.h | 6 +++--- src/core/platform/tests/test-common.c | 6 +++--- src/core/platform/tests/test-common.h | 4 ++-- src/libnm-platform/nmp-base.h | 6 +++--- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/nm-ip-config.c b/src/core/nm-ip-config.c index 51a069f801..4616f2e4a3 100644 --- a/src/core/nm-ip-config.c +++ b/src/core/nm-ip-config.c @@ -56,7 +56,7 @@ G_DEFINE_ABSTRACT_TYPE(NMIPConfig, nm_ip_config, NM_TYPE_DBUS_OBJECT) /*****************************************************************************/ -static void _handle_platform_change(NMIPConfig *self, guint32 obj_type_flags, gboolean is_init); +static void _handle_platform_change(NMIPConfig *self, guint64 obj_type_flags, gboolean is_init); static void _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd); /*****************************************************************************/ @@ -77,7 +77,7 @@ static void _notify_platform_handle(NMIPConfig *self, gint64 now_msec) { NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self); - guint32 obj_type_flags; + guint64 obj_type_flags; nm_clear_g_source_inst(&priv->notify_platform_timeout_source); @@ -98,7 +98,7 @@ _notify_platform_cb(gpointer user_data) } static void -_notify_platform(NMIPConfig *self, guint32 obj_type_flags) +_notify_platform(NMIPConfig *self, guint64 obj_type_flags) { const int addr_family = nm_ip_config_get_addr_family(self); const int IS_IPv4 = NM_IS_IPv4(addr_family); @@ -953,7 +953,7 @@ _handle_l3cd_changed(NMIPConfig *self, const NML3ConfigData *l3cd) } static void -_handle_platform_change(NMIPConfig *self, guint32 obj_type_flags, gboolean is_init) +_handle_platform_change(NMIPConfig *self, guint64 obj_type_flags, gboolean is_init) { const int addr_family = nm_ip_config_get_addr_family(self); const int IS_IPv4 = NM_IS_IPv4(addr_family); diff --git a/src/core/nm-ip-config.h b/src/core/nm-ip-config.h index 47a40bd223..02d654c4fd 100644 --- a/src/core/nm-ip-config.h +++ b/src/core/nm-ip-config.h @@ -35,7 +35,7 @@ struct _NMIPConfigPrivate { GSource *notify_platform_timeout_source; gint64 notify_platform_rlimited_until_msec; gulong l3cfg_notify_id; - guint32 notify_platform_obj_type_flags; + guint64 notify_platform_obj_type_flags; }; struct _NMIPConfig { diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c index 9bb272df10..59fc6e4d7f 100644 --- a/src/core/nm-l3cfg.c +++ b/src/core/nm-l3cfg.c @@ -657,7 +657,7 @@ _l3_config_notify_data_to_string(const NML3ConfigNotifyData *notify_data, case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE: nm_strbuf_append(&s, &l, - ", obj-type-flags=0x%x", + ", obj-type-flags=0x%" G_GINT64_MODIFIER "x", notify_data->platform_change_on_idle.obj_type_flags); break; case NM_L3_CONFIG_NOTIFY_TYPE_IPV4LL_EVENT: @@ -1604,7 +1604,7 @@ _load_link(NML3Cfg *self, gboolean initial) /*****************************************************************************/ void -_nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint32 obj_type_flags) +_nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint64 obj_type_flags) { NML3ConfigNotifyData notify_data; diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h index 5f0721da3c..c103c9f233 100644 --- a/src/core/nm-l3cfg.h +++ b/src/core/nm-l3cfg.h @@ -178,7 +178,7 @@ typedef struct { } platform_change; struct { - guint32 obj_type_flags; + guint64 obj_type_flags; } platform_change_on_idle; struct { @@ -207,7 +207,7 @@ struct _NML3Cfg { * NML3Cfg instance. We track some per-l3cfg-data that is only * relevant to NMNetns here. */ struct { - guint32 signal_pending_obj_type_flags; + guint64 signal_pending_obj_type_flags; CList signal_pending_lst; CList ecmp_track_ifindex_lst_head; } internal_netns; @@ -223,7 +223,7 @@ NML3Cfg *nm_l3cfg_new(NMNetns *netns, int ifindex); gboolean nm_l3cfg_is_ready(NML3Cfg *self); -void _nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint32 obj_type_flags); +void _nm_l3cfg_notify_platform_change_on_idle(NML3Cfg *self, guint64 obj_type_flags); void _nm_l3cfg_notify_platform_change(NML3Cfg *self, NMPlatformSignalChangeType change_type, diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c index 91445b30f8..3035bd491d 100644 --- a/src/core/platform/tests/test-common.c +++ b/src/core/platform/tests/test-common.c @@ -1230,7 +1230,7 @@ out: } gboolean -nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolean do_assert) +nmtstp_check_platform_full(NMPlatform *platform, guint64 obj_type_flags, gboolean do_assert) { static const NMPObjectType obj_types[] = { NMP_OBJECT_TYPE_IP4_ADDRESS, @@ -1265,7 +1265,7 @@ nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolea for (i_obj_types = 0; i_obj_types < (int) G_N_ELEMENTS(obj_types); i_obj_types++) { const NMPObjectType obj_type = obj_types[i_obj_types]; - const guint32 i_obj_type_flags = nmp_object_type_to_flags(obj_type); + const guint64 i_obj_type_flags = nmp_object_type_to_flags(obj_type); gs_unref_ptrarray GPtrArray *arr1 = NULL; gs_unref_ptrarray GPtrArray *arr2 = NULL; NMPLookup lookup; @@ -1408,7 +1408,7 @@ nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolea } void -nmtstp_check_platform(NMPlatform *platform, guint32 obj_type_flags) +nmtstp_check_platform(NMPlatform *platform, guint64 obj_type_flags) { if (!nmtstp_check_platform_full(platform, obj_type_flags, FALSE)) { /* It's unclear why this failure sometimes happens. It happens diff --git a/src/core/platform/tests/test-common.h b/src/core/platform/tests/test-common.h index 85ed79615b..fcda583d2a 100644 --- a/src/core/platform/tests/test-common.h +++ b/src/core/platform/tests/test-common.h @@ -140,9 +140,9 @@ int nmtstp_run_command(const char *format, ...) _nm_printf(1, 2); /*****************************************************************************/ gboolean -nmtstp_check_platform_full(NMPlatform *platform, guint32 obj_type_flags, gboolean do_assert); +nmtstp_check_platform_full(NMPlatform *platform, guint64 obj_type_flags, gboolean do_assert); -void nmtstp_check_platform(NMPlatform *platform, guint32 obj_type_flags); +void nmtstp_check_platform(NMPlatform *platform, guint64 obj_type_flags); /*****************************************************************************/ diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h index f665629645..3dec106a50 100644 --- a/src/libnm-platform/nmp-base.h +++ b/src/libnm-platform/nmp-base.h @@ -194,15 +194,15 @@ typedef enum _nm_packed { NMP_OBJECT_TYPE_MAX = __NMP_OBJECT_TYPE_LAST - 1, } NMPObjectType; -static inline guint32 +static inline guint64 nmp_object_type_to_flags(NMPObjectType obj_type) { - G_STATIC_ASSERT_EXPR(NMP_OBJECT_TYPE_MAX < 32); + G_STATIC_ASSERT_EXPR(NMP_OBJECT_TYPE_MAX < 64); nm_assert(_NM_INT_NOT_NEGATIVE(obj_type)); nm_assert(obj_type < NMP_OBJECT_TYPE_MAX); - return ((guint32) 1u) << obj_type; + return ((guint64) 1u) << obj_type; } /*****************************************************************************/ From 39a1baa2891d4f3d7651911000b1d6d952d77240 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:14:26 +0100 Subject: [PATCH 2/9] platform: add nexthop support Add support for route nexthop objects to the platform. Contrary to other object types, we don't track nexthops in the platform cache because on some systems there could be many of them and this can become very inefficient. To configure a desired list of nexthops for an interface it is necessary to first dump the current list, delete the unwanted ones and add the desired ones. The dump returns only the nexthops for the given interface and address family. --- src/core/platform/tests/test-route.c | 177 +++++++++++++++ src/libnm-platform/nm-linux-platform.c | 292 ++++++++++++++++++++++++- src/libnm-platform/nm-netlink.c | 3 + src/libnm-platform/nm-platform.c | 217 ++++++++++++++++++ src/libnm-platform/nm-platform.h | 31 +++ src/libnm-platform/nmp-base.h | 7 + src/libnm-platform/nmp-object.c | 108 +++++++++ src/libnm-platform/nmp-object.h | 26 +++ src/libnm-platform/nmp-plobj.c | 100 +++++++++ src/libnm-platform/nmp-plobj.h | 52 +++++ 10 files changed, 1012 insertions(+), 1 deletion(-) diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c index fbad2447a9..78301ffec6 100644 --- a/src/core/platform/tests/test-route.c +++ b/src/core/platform/tests/test-route.c @@ -2239,6 +2239,175 @@ test_mptcp(gconstpointer test_data) /*****************************************************************************/ +static void +test_nexthop_dump(void) +{ + const int ifindex1 = NMTSTP_ENV1_IFINDEXES[0]; + const int ifindex2 = NMTSTP_ENV1_IFINDEXES[1]; + const char *ifname1 = NMTSTP_ENV1_DEVICE_NAME[0]; + const char *ifname2 = NMTSTP_ENV1_DEVICE_NAME[1]; + GPtrArray *result; + const NMPObject *obj; + + nmtstp_run_command_check("ip addr add 1.2.3.0/24 dev %s", ifname1); + nmtstp_run_command_check("ip addr add fe80::1/64 dev %s", ifname1); + nmtstp_run_command_check("ip addr add fe80::2/64 dev %s", ifname2); + + nmtstp_run_command_check("ip nexthop add id 4 dev %s", ifname1); + nmtstp_run_command_check("ip nexthop add id 5 dev %s via 1.2.3.4", ifname1); + nmtstp_run_command_check("ip nexthop add id 6 dev %s", ifname2); + + nmtstp_run_command_check("ip -6 nexthop add id 12345670 dev %s", ifname1); + nmtstp_run_command_check("ip -6 nexthop add id 12345671 dev %s via fe80::11", ifname1); + nmtstp_run_command_check("ip -6 nexthop add id 12345672 dev %s via fe80::12", ifname1); + nmtstp_run_command_check("ip -6 nexthop add id 12345673 dev %s via fe80::11", ifname2); + + /* interface 1, IPv4 */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET, ifindex1); + g_assert(result); + g_assert_cmpint(result->len, ==, 2); + obj = result->pdata[0]; + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->id, ==, 4); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->ifindex, ==, ifindex1); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->gateway, ==, 0); + obj = result->pdata[1]; + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->id, ==, 5); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->ifindex, ==, ifindex1); + nmtst_assert_ip4_address(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->gateway, "1.2.3.4"); + g_ptr_array_unref(result); + + /* interface 1, IPv6 */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET6, ifindex1); + g_assert(result); + g_assert_cmpint(result->len, ==, 3); + obj = result->pdata[0]; + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->id, ==, 12345670); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->ifindex, ==, ifindex1); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->gateway, "::"); + obj = result->pdata[1]; + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->id, ==, 12345671); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->ifindex, ==, ifindex1); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->gateway, "fe80::11"); + obj = result->pdata[2]; + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->id, ==, 12345672); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->ifindex, ==, ifindex1); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->gateway, "fe80::12"); + g_ptr_array_unref(result); + + /* interface 2, IPv4 */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET, ifindex2); + g_assert(result); + g_assert_cmpint(result->len, ==, 1); + obj = result->pdata[0]; + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->id, ==, 6); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->ifindex, ==, ifindex2); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(obj)->gateway, ==, 0); + g_ptr_array_unref(result); + + /* interface 2, IPv6 */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET6, ifindex2); + g_assert(result); + g_assert_cmpint(result->len, ==, 1); + obj = result->pdata[0]; + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->id, ==, 12345673); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->ifindex, ==, ifindex2); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(obj)->gateway, "fe80::11"); + g_ptr_array_unref(result); +} + +/*****************************************************************************/ + +static void +test_nexthop_add(void) +{ + const int ifindex = NMTSTP_ENV1_IFINDEXES[0]; + const char *ifname = NMTSTP_ENV1_DEVICE_NAME[0]; + NMPObject obj; + GPtrArray *result; + int r; + + nmtstp_run_command_check("ip addr add 1.2.3.0/24 dev %s", ifname); + nmtstp_run_command_check("ip addr add fe80::1/64 dev %s", ifname); + + /* Add IPv4 nexthop without gateway */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP4_NEXTHOP, NULL); + obj.ip4_nexthop.id = 100; + obj.ip4_nexthop.ifindex = ifindex; + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Add IPv4 nexthop with gateway */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP4_NEXTHOP, NULL); + obj.ip4_nexthop.id = 101; + obj.ip4_nexthop.ifindex = ifindex; + obj.ip4_nexthop.gateway = nmtst_inet4_from_string("1.2.3.4"); + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Add IPv6 nexthop without gateway */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_NEXTHOP, NULL); + obj.ip6_nexthop.id = 200; + obj.ip6_nexthop.ifindex = ifindex; + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Add IPv6 nexthop with gateway */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_NEXTHOP, NULL); + obj.ip6_nexthop.id = 201; + obj.ip6_nexthop.ifindex = ifindex; + obj.ip6_nexthop.gateway = nmtst_inet6_from_string("fe80::99"); + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Verify IPv4 nexthops via dump */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET, ifindex); + g_assert(result); + g_assert_cmpint(result->len, ==, 2); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[0])->id, ==, 100); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[0])->ifindex, ==, ifindex); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[0])->gateway, ==, 0); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[1])->id, ==, 101); + g_assert_cmpint(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[1])->ifindex, ==, ifindex); + nmtst_assert_ip4_address(NMP_OBJECT_CAST_IP4_NEXTHOP(result->pdata[1])->gateway, "1.2.3.4"); + g_ptr_array_unref(result); + + /* Verify IPv6 nexthops via dump */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET6, ifindex); + g_assert(result); + g_assert_cmpint(result->len, ==, 2); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[0])->id, ==, 200); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[0])->ifindex, ==, ifindex); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[0])->gateway, "::"); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[1])->id, ==, 201); + g_assert_cmpint(NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[1])->ifindex, ==, ifindex); + nmtst_assert_ip6_address(&NMP_OBJECT_CAST_IP6_NEXTHOP(result->pdata[1])->gateway, "fe80::99"); + g_ptr_array_unref(result); + + /* Delete all nexthops */ + g_assert( + nm_platform_object_delete(NM_PLATFORM_GET, nmp_object_stackinit_id_ip4_nexthop(&obj, 100))); + g_assert( + nm_platform_object_delete(NM_PLATFORM_GET, nmp_object_stackinit_id_ip4_nexthop(&obj, 101))); + g_assert( + nm_platform_object_delete(NM_PLATFORM_GET, nmp_object_stackinit_id_ip6_nexthop(&obj, 200))); + g_assert( + nm_platform_object_delete(NM_PLATFORM_GET, nmp_object_stackinit_id_ip6_nexthop(&obj, 201))); + + /* Verify IPv4 nexthops are gone */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET, ifindex); + g_assert(result); + g_assert_cmpint(result->len, ==, 0); + g_ptr_array_unref(result); + + /* Verify IPv6 nexthops are gone */ + result = nm_platform_ip_nexthop_dump(NM_PLATFORM_GET, AF_INET6, ifindex); + g_assert(result); + g_assert_cmpint(result->len, ==, 0); + g_ptr_array_unref(result); +} + +/*****************************************************************************/ + static void _ensure_onlink_routes(void) { @@ -2474,6 +2643,8 @@ void _nmtstp_setup_tests(void) { #define add_test_func(testpath, test_func) nmtstp_env1_add_test_func(testpath, test_func, 1, TRUE) +#define add_test_func_with_if2(testpath, test_func) \ + nmtstp_env1_add_test_func(testpath, test_func, 2, TRUE) #define add_test_func_data(testpath, test_func, arg) \ nmtstp_env1_add_test_func_data(testpath, test_func, arg, 1, TRUE) #define add_test_func_data_with_if2(testpath, test_func, arg) \ @@ -2511,6 +2682,12 @@ _nmtstp_setup_tests(void) add_test_func_data("/route/mptcp/1", test_mptcp, GINT_TO_POINTER(1)); add_test_func_data("/route/mptcp/2", test_mptcp, GINT_TO_POINTER(2)); } + + if (nmtstp_is_root_test()) { + add_test_func_with_if2("/route/nexthop/dump", test_nexthop_dump); + add_test_func("/route/nexthop/add", test_nexthop_add); + } + if (nmtstp_is_root_test()) { add_test_func_data_with_if2("/route/test_cache_consistency_routes/1", test_cache_consistency_routes, diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c index cc5b99e095..95200182ec 100644 --- a/src/libnm-platform/nm-linux-platform.c +++ b/src/libnm-platform/nm-linux-platform.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -4420,6 +4421,62 @@ rta_multipath_done: return g_steal_pointer(&obj); } +static NMPObject * +_new_from_nl_nexthop(const struct nlmsghdr *nlh, gboolean id_only, gboolean tracked_protos_only) +{ + static const struct nla_policy policy[] = { + [NHA_ID] = {.type = NLA_U32}, + [NHA_OIF] = {.type = NLA_U32}, + [NHA_GATEWAY] = {.maxlen = sizeof(struct in6_addr)}, + }; + const struct nhmsg *nhm; + gboolean IS_IPv4; + struct nlattr *tb[G_N_ELEMENTS(policy)]; + nm_auto_nmpobj NMPObject *obj = NULL; + + if (!nlmsg_valid_hdr(nlh, sizeof(*nhm))) + return NULL; + + nhm = nlmsg_data(nlh); + + if (tracked_protos_only && !NM_IN_SET(nhm->nh_protocol, IP_ROUTE_TRACKED_PROTOCOLS)) { + return NULL; + } + + if (nhm->nh_family == AF_INET) + IS_IPv4 = TRUE; + else if (nhm->nh_family == AF_INET6) + IS_IPv4 = FALSE; + else + return NULL; + + if (nlmsg_parse_arr(nlh, sizeof(*nhm), tb, policy) < 0) + return NULL; + + if (!tb[NHA_ID]) + return NULL; + + obj = nmp_object_new(NMP_OBJECT_TYPE_IP_NEXTHOP(IS_IPv4), NULL); + obj->ip_nexthop.id = nla_get_u32(tb[NHA_ID]); + + if (id_only) + return g_steal_pointer(&obj); + + obj->ip_nexthop.nh_source = nmp_utils_ip_config_source_from_rtprot(nhm->nh_protocol); + if (tb[NHA_OIF]) + obj->ip_nexthop.ifindex = nla_get_u32(tb[NHA_OIF]); + + if (IS_IPv4) { + if (tb[NHA_GATEWAY] && nla_len(tb[NHA_GATEWAY]) == sizeof(in_addr_t)) + obj->ip4_nexthop.gateway = nla_get_u32(tb[NHA_GATEWAY]); + return g_steal_pointer(&obj); + } else { + if (tb[NHA_GATEWAY] && nla_len(tb[NHA_GATEWAY]) == sizeof(struct in6_addr)) + nla_memcpy(&obj->ip6_nexthop.gateway, tb[NHA_GATEWAY], sizeof(struct in6_addr)); + return g_steal_pointer(&obj); + } +} + static NMPObject * _new_from_nl_routing_rule(const struct nlmsghdr *nlh, gboolean id_only) { @@ -4885,6 +4942,10 @@ nmp_object_new_from_nl(NMPlatform *platform, case RTM_DELRULE: case RTM_GETRULE: return _new_from_nl_routing_rule(msghdr, id_only); + case RTM_NEWNEXTHOP: + case RTM_DELNEXTHOP: + case RTM_GETNEXTHOP: + return _new_from_nl_nexthop(msghdr, id_only, FALSE); case RTM_NEWQDISC: case RTM_DELQDISC: case RTM_GETQDISC: @@ -5707,6 +5768,54 @@ ip_route_is_alive(const NMPlatformIPRoute *route) return ip_route_is_tracked(proto, type); } +static struct nl_msg * +_nl_msg_new_nexthop(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *obj) +{ + nm_auto_nlmsg struct nl_msg *msg = NULL; + const NMPClass *klass = NMP_OBJECT_GET_CLASS(obj); + const gboolean IS_IPv4 = NM_IS_IPv4(klass->addr_family); + struct nhmsg nhm = { + .nh_family = klass->addr_family, + }; + + nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj), + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP)); + nm_assert(NM_IN_SET(nlmsg_type, RTM_NEWNEXTHOP, RTM_DELNEXTHOP)); + + msg = nlmsg_alloc_new(0, nlmsg_type, nlmsg_flags); + + if (nlmsg_type == RTM_NEWNEXTHOP) { + /* the protocol must be zero for deletion messages */ + nhm.nh_protocol = nmp_utils_ip_config_source_coerce_to_rtprot(obj->ip_nexthop.nh_source); + } + + if (nlmsg_append_struct(msg, &nhm) < 0) + goto nla_put_failure; + + NLA_PUT_U32(msg, NHA_ID, obj->ip_nexthop.id); + + if (nlmsg_type == RTM_DELNEXTHOP) + goto end; + + if (obj->ip_nexthop.ifindex > 0) + NLA_PUT_U32(msg, NHA_OIF, obj->ip_nexthop.ifindex); + + if (IS_IPv4) { + if (obj->ip4_nexthop.gateway != INADDR_ANY) + NLA_PUT(msg, NHA_GATEWAY, sizeof(in_addr_t), &obj->ip4_nexthop.gateway); + } else { + if (!IN6_IS_ADDR_UNSPECIFIED(&obj->ip6_nexthop.gateway)) + NLA_PUT(msg, NHA_GATEWAY, sizeof(struct in6_addr), &obj->ip6_nexthop.gateway); + } + +end: + return g_steal_pointer(&msg); + +nla_put_failure: + g_return_val_if_reached(NULL); +} + /* Copied and modified from libnl3's build_route_msg() and rtnl_route_build_msg(). */ static struct nl_msg * _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *obj) @@ -8548,7 +8657,9 @@ do_add_addrroute(NMPlatform *platform, NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP6_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE, - NMP_OBJECT_TYPE_IP6_ROUTE)); + NMP_OBJECT_TYPE_IP6_ROUTE, + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP)); event_handler_read_netlink(platform, NMP_NETLINK_ROUTE, FALSE); @@ -10723,6 +10834,177 @@ ip6_address_delete(NMPlatform *platform, int ifindex, struct in6_addr addr, guin /*****************************************************************************/ +static int +ip_nexthop_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack, char **out_extack_msg) +{ + nm_auto_nlmsg struct nl_msg *nlmsg = NULL; + + nlmsg = _nl_msg_new_nexthop(RTM_NEWNEXTHOP, flags & NMP_NLM_FLAG_FMASK, obj_stack); + if (!nlmsg) + g_return_val_if_reached(-NME_BUG); + return do_add_addrroute(platform, + obj_stack, + nlmsg, + NM_FLAGS_HAS(flags, NMP_NLM_FLAG_SUPPRESS_NETLINK_FAILURE), + out_extack_msg); +} + +typedef struct { + GPtrArray *result; + int addr_family; + int ifindex; +} NexthopDumpData; + +static int +_ip_nexthop_dump_parse_cb(const struct nl_msg *msg, void *arg) +{ + NexthopDumpData *data = arg; + nm_auto_nmpobj NMPObject *obj = NULL; + + obj = _new_from_nl_nexthop(nlmsg_hdr(msg), FALSE, TRUE); + if (!obj) + return NL_SKIP; + + g_ptr_array_add(data->result, g_steal_pointer(&obj)); + return NL_OK; +} + +static GPtrArray * +ip_nexthop_dump(NMPlatform *platform, int addr_family, int ifindex) +{ + nm_auto_nlmsg struct nl_msg *nlmsg = NULL; + struct nl_sock *sk = NULL; + struct nhmsg nhm = { + .nh_family = addr_family, + }; + NexthopDumpData data; + int nle; + + nlmsg = nlmsg_alloc_new(0, RTM_GETNEXTHOP, NLM_F_DUMP); + if (!nlmsg) + g_return_val_if_reached(NULL); + + if (nlmsg_append_struct(nlmsg, &nhm) < 0) + g_return_val_if_reached(NULL); + + NLA_PUT_U32(nlmsg, NHA_OIF, ifindex); + + nle = nl_socket_new(&sk, NETLINK_ROUTE, NL_SOCKET_FLAGS_DISABLE_MSG_PEEK, 0, 0); + if (nle < 0) { + _LOGD("nexthop-dump: error opening socket: %s (%d)", nm_strerror(nle), nle); + return NULL; + } + + nle = nl_send_auto(sk, nlmsg); + if (nle < 0) { + _LOGD("nexthop-dump: failed sending request: %s (%d)", nm_strerror(nle), nle); + nl_socket_free(sk); + return NULL; + } + + data = (NexthopDumpData) { + .result = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref), + .addr_family = addr_family, + .ifindex = ifindex, + }; + + do { + nle = nl_recvmsgs(sk, + &((const struct nl_cb) { + .valid_cb = _ip_nexthop_dump_parse_cb, + .valid_arg = &data, + })); + } while (nle == -EAGAIN); + + nl_socket_free(sk); + + if (nle < 0) { + _LOGD("nexthop-dump: recv failed: %s (%d)", nm_strerror(nle), nle); + g_ptr_array_unref(data.result); + return NULL; + } + + return data.result; +nla_put_failure: + g_return_val_if_reached(NULL); +} + +typedef struct { + NMPObject *obj; +} NexthopGetData; + +static int +_ip_nexthop_get_parse_cb(const struct nl_msg *msg, void *arg) +{ + NexthopGetData *data = arg; + + nm_assert(!data->obj); + + data->obj = _new_from_nl_nexthop(nlmsg_hdr(msg), FALSE, FALSE); + if (!data->obj) + return NL_SKIP; + + return NL_OK; +} + +static gboolean +ip_nexthop_get(NMPlatform *platform, guint32 nh_id, NMPObject **out_obj) +{ + nm_auto_nlmsg struct nl_msg *nlmsg = NULL; + struct nl_sock *sk = NULL; + struct nhmsg nhm = { + .nh_family = AF_UNSPEC, + }; + NexthopGetData data = { + .obj = NULL, + }; + int nle; + + nlmsg = nlmsg_alloc_new(0, RTM_GETNEXTHOP, 0); + if (!nlmsg) + g_return_val_if_reached(FALSE); + + if (nlmsg_append_struct(nlmsg, &nhm) < 0) + g_return_val_if_reached(FALSE); + + NLA_PUT_U32(nlmsg, NHA_ID, nh_id); + + nle = nl_socket_new(&sk, NETLINK_ROUTE, NL_SOCKET_FLAGS_DISABLE_MSG_PEEK, 0, 0); + if (nle < 0) { + _LOGD("nexthop-get: error opening socket: %s (%d)", nm_strerror(nle), nle); + return FALSE; + } + + nle = nl_send_auto(sk, nlmsg); + if (nle < 0) { + _LOGD("nexthop-get: failed sending request: %s (%d)", nm_strerror(nle), nle); + nl_socket_free(sk); + return FALSE; + } + + nle = nl_recvmsgs(sk, + &((const struct nl_cb) { + .valid_cb = _ip_nexthop_get_parse_cb, + .valid_arg = &data, + })); + + nl_socket_free(sk); + + if (nle < 0) { + nm_clear_pointer(&data.obj, nmp_object_unref); + return FALSE; + } + + if (!data.obj) + return FALSE; + + NM_SET_OUT(out_obj, data.obj); + return TRUE; + +nla_put_failure: + g_return_val_if_reached(FALSE); +} + static int ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack, char **out_extack_msg) { @@ -10752,6 +11034,10 @@ object_delete(NMPlatform *platform, const NMPObject *obj) case NMP_OBJECT_TYPE_IP6_ROUTE: nlmsg = _nl_msg_new_route(RTM_DELROUTE, 0, obj); break; + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + case NMP_OBJECT_TYPE_IP6_NEXTHOP: + nlmsg = _nl_msg_new_nexthop(RTM_DELNEXTHOP, 0, obj); + break; case NMP_OBJECT_TYPE_ROUTING_RULE: nlmsg = _nl_msg_new_routing_rule(RTM_DELRULE, 0, NMP_OBJECT_CAST_ROUTING_RULE(obj)); break; @@ -12438,6 +12724,10 @@ nm_linux_platform_class_init(NMLinuxPlatformClass *klass) platform_class->ip_route_add = ip_route_add; platform_class->ip_route_get = ip_route_get; + platform_class->ip_nexthop_add = ip_nexthop_add; + platform_class->ip_nexthop_dump = ip_nexthop_dump; + platform_class->ip_nexthop_get = ip_nexthop_get; + platform_class->routing_rule_add = routing_rule_add; platform_class->qdisc_add = qdisc_add; diff --git a/src/libnm-platform/nm-netlink.c b/src/libnm-platform/nm-netlink.c index 9eb6721f0e..be0c58043b 100644 --- a/src/libnm-platform/nm-netlink.c +++ b/src/libnm-platform/nm-netlink.c @@ -94,6 +94,9 @@ static NM_UTILS_LOOKUP_STR_DEFINE(_rtnl_type_to_str, NM_UTILS_LOOKUP_STR_ITEM(RTM_GETRULE, "RTM_GETRULE"), NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWRULE, "RTM_NEWRULE"), NM_UTILS_LOOKUP_STR_ITEM(RTM_DELRULE, "RTM_DELRULE"), + NM_UTILS_LOOKUP_STR_ITEM(RTM_GETNEXTHOP, "RTM_GETNEXTHOP"), + NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWNEXTHOP, "RTM_NEWNEXTHOP"), + NM_UTILS_LOOKUP_STR_ITEM(RTM_DELNEXTHOP, "RTM_DELNEXTHOP"), NM_UTILS_LOOKUP_STR_ITEM(RTM_GETQDISC, "RTM_GETQDISC"), NM_UTILS_LOOKUP_STR_ITEM(RTM_NEWQDISC, "RTM_NEWQDISC"), NM_UTILS_LOOKUP_STR_ITEM(RTM_DELQDISC, "RTM_DELQDISC"), diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c index 853157d204..8ecadea16c 100644 --- a/src/libnm-platform/nm-platform.c +++ b/src/libnm-platform/nm-platform.c @@ -4770,6 +4770,84 @@ next_plat:; return success; } +gboolean +nm_platform_ip_nexthop_sync(NMPlatform *self, + int addr_family, + int ifindex, + GPtrArray *known_nexthops, + GPtrArray *nexthops_prune, + GPtrArray *nexthops_platform) +{ + gs_unref_hashtable GHashTable *known_nexthops_idx = NULL; + gs_unref_hashtable GHashTable *nexthops_platform_idx = NULL; + int IS_IPv4 = NM_IS_IPv4(addr_family); + guint i; + gboolean success = TRUE; + + if (known_nexthops && known_nexthops->len > 0) { + known_nexthops_idx = g_hash_table_new(nm_direct_hash, NULL); + for (i = 0; i < known_nexthops->len; i++) { + const NMPObject *nh = known_nexthops->pdata[i]; + + g_hash_table_add(known_nexthops_idx, GUINT_TO_POINTER(nh->ipx_nexthop.nhx.id)); + } + } + + if (nexthops_platform && nexthops_platform->len > 0) { + nexthops_platform_idx = g_hash_table_new(nm_direct_hash, NULL); + for (i = 0; i < nexthops_platform->len; i++) { + const NMPObject *nh = nexthops_platform->pdata[i]; + + g_hash_table_insert(nexthops_platform_idx, + GUINT_TO_POINTER(nh->ipx_nexthop.nhx.id), + (gpointer) nh); + } + } + + if (nexthops_prune) { + for (i = 0; i < nexthops_prune->len; i++) { + const NMPObject *prune_o = nexthops_prune->pdata[i]; + + nm_assert((IS_IPv4 && NMP_OBJECT_GET_TYPE(prune_o) == NMP_OBJECT_TYPE_IP4_NEXTHOP) + || (!IS_IPv4 && NMP_OBJECT_GET_TYPE(prune_o) == NMP_OBJECT_TYPE_IP6_NEXTHOP)); + + if (nm_g_hash_table_lookup(known_nexthops_idx, + GUINT_TO_POINTER(NMP_OBJECT_CAST_IP_NEXTHOP(prune_o)->id))) + continue; + + if (!nm_platform_object_delete(self, prune_o)) { + /* ignore error... */ + } + } + } + + if (known_nexthops) { + for (i = 0; i < known_nexthops->len; i++) { + const NMPObject *nh = known_nexthops->pdata[i]; + const NMPObject *candidate; + + candidate = + nm_g_hash_table_lookup(nexthops_platform_idx, GUINT_TO_POINTER(nh->ip_nexthop.id)); + if (candidate + && (IS_IPv4 + ? nm_platform_ip4_nexthop_cmp(&nh->ip4_nexthop, + &candidate->ip4_nexthop, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY) + : nm_platform_ip6_nexthop_cmp(&nh->ip6_nexthop, + &candidate->ip6_nexthop, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY)) + == 0) { + continue; + } + + if (nm_platform_ip_nexthop_add(self, NMP_NLM_FLAG_REPLACE, nh, NULL) != 0) + success = FALSE; + } + } + + return success; +} + gboolean nm_platform_ip_address_flush(NMPlatform *self, int addr_family, int ifindex) { @@ -5364,6 +5442,69 @@ nm_platform_ip_route_flush(NMPlatform *self, int addr_family, int ifindex) return success; } +GPtrArray * +nm_platform_ip_nexthop_dump(NMPlatform *self, int addr_family, int ifindex) +{ + _CHECK_SELF(self, klass, NULL); + + nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6)); + + return klass->ip_nexthop_dump(self, addr_family, ifindex); +} + +gboolean +nm_platform_ip_nexthop_get(NMPlatform *self, guint32 nh_id, NMPObject **out_obj) +{ + nm_auto_nmpobj NMPObject *obj = NULL; + gboolean result; + + _CHECK_SELF(self, klass, FALSE); + + nm_assert(nh_id > 0); + + if (!klass->ip_nexthop_get) { + NM_SET_OUT(out_obj, NULL); + return FALSE; + } + + result = klass->ip_nexthop_get(self, nh_id, &obj); + + if (result) { + nm_assert(obj); + _LOGD("nexthop: get nexthop %u succeeded", nh_id); + NM_SET_OUT(out_obj, g_steal_pointer(&obj)); + } else { + _LOGD("nexthop: get nexthop %u failed", nh_id); + NM_SET_OUT(out_obj, NULL); + } + + return result; +} + +gboolean +nm_platform_ip_nexthop_flush(NMPlatform *self, int addr_family, int ifindex) +{ + gboolean success = TRUE; + + _CHECK_SELF(self, klass, FALSE); + + nm_assert(NM_IN_SET(addr_family, AF_UNSPEC, AF_INET, AF_INET6)); + + if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET)) { + gs_unref_ptrarray GPtrArray *nexthops_prune = NULL; + + nexthops_prune = nm_platform_ip_nexthop_dump(self, AF_INET, ifindex); + success &= nm_platform_ip_nexthop_sync(self, AF_INET, ifindex, NULL, nexthops_prune, NULL); + } + if (NM_IN_SET(addr_family, AF_UNSPEC, AF_INET6)) { + gs_unref_ptrarray GPtrArray *nexthops_prune = NULL; + + nexthops_prune = nm_platform_ip_nexthop_dump(self, AF_INET6, ifindex); + success &= nm_platform_ip_nexthop_sync(self, AF_INET6, ifindex, NULL, nexthops_prune, NULL); + } + return success; +} + /*****************************************************************************/ static guint8 @@ -5617,6 +5758,34 @@ nm_platform_ip6_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformI return _ip_route_add(self, flags, &obj, NULL); } +int +nm_platform_ip_nexthop_add(NMPlatform *self, + NMPNlmFlags flags, + const NMPObject *obj, + char **out_extack_msg) +{ + NMPObject obj_stack; + char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE]; + int ifindex; + + _CHECK_SELF(self, klass, -NME_BUG); + + nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj), + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP)); + + nmp_object_stackinit(&obj_stack, NMP_OBJECT_GET_TYPE(obj), &obj->ip_nexthop); + + ifindex = obj_stack.ip_nexthop.ifindex; + + _LOG3D("nexthop: %-10s IPv%c nexthop: %s", + _nmp_nlm_flag_to_string(flags & NMP_NLM_FLAG_FMASK), + nm_utils_addr_family_to_char(NMP_OBJECT_GET_ADDR_FAMILY(&obj_stack)), + nmp_object_to_string(&obj_stack, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf))); + + return klass->ip_nexthop_add(self, flags, &obj_stack, out_extack_msg); +} + gboolean nm_platform_object_delete(NMPlatform *self, const NMPObject *obj) { @@ -5635,6 +5804,8 @@ nm_platform_object_delete(NMPlatform *self, const NMPObject *obj) break; case NMP_OBJECT_TYPE_IP4_ROUTE: case NMP_OBJECT_TYPE_IP6_ROUTE: + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + case NMP_OBJECT_TYPE_IP6_NEXTHOP: case NMP_OBJECT_TYPE_QDISC: case NMP_OBJECT_TYPE_TFILTER: ifindex = NMP_OBJECT_CAST_OBJ_WITH_IFINDEX(obj)->ifindex; @@ -7380,6 +7551,52 @@ nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route *route, return buf0; } +const char * +nm_platform_ip4_nexthop_to_string(const NMPlatformIP4NextHop *nexthop, char *buf, gsize len) +{ + char s_gateway[INET_ADDRSTRLEN]; + char s_source[50]; + + if (!nm_utils_to_string_buffer_init_null(nexthop, &buf, &len)) + return buf; + + inet_ntop(AF_INET, &nexthop->gateway, s_gateway, sizeof(s_gateway)); + nmp_utils_ip_config_source_to_string(nexthop->nh_source, s_source, sizeof(s_source)); + + g_snprintf(buf, + len, + "id %u dev %d gw %s rt-src %s", + nexthop->id, + nexthop->ifindex, + s_gateway, + s_source); + + return buf; +} + +const char * +nm_platform_ip6_nexthop_to_string(const NMPlatformIP6NextHop *nexthop, char *buf, gsize len) +{ + char s_gateway[INET6_ADDRSTRLEN]; + char s_source[50]; + + if (!nm_utils_to_string_buffer_init_null(nexthop, &buf, &len)) + return buf; + + inet_ntop(AF_INET6, &nexthop->gateway, s_gateway, sizeof(s_gateway)); + nmp_utils_ip_config_source_to_string(nexthop->nh_source, s_source, sizeof(s_source)); + + g_snprintf(buf, + len, + "id %u dev %d gw %s rt-src %s", + nexthop->id, + nexthop->ifindex, + s_gateway, + s_source); + + return buf; +} + /** * nm_platform_ip6_route_to_string: * @route: pointer to NMPlatformIP6Route route structure diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h index 033dc51541..338e621642 100644 --- a/src/libnm-platform/nm-platform.h +++ b/src/libnm-platform/nm-platform.h @@ -1339,6 +1339,15 @@ typedef struct { int oif_ifindex, NMPObject **out_route); + int (*ip_nexthop_add)(NMPlatform *self, + NMPNlmFlags flags, + NMPObject *obj_stack, + char **out_extack_msg); + + GPtrArray *(*ip_nexthop_dump)(NMPlatform *self, int addr_family, int ifindex); + + gboolean (*ip_nexthop_get)(NMPlatform *self, guint32 nh_id, NMPObject **out_obj); + int (*routing_rule_add)(NMPlatform *self, NMPNlmFlags flags, const NMPlatformRoutingRule *routing_rule); @@ -2444,6 +2453,11 @@ int nm_platform_ip4_route_add(NMPlatform *self, const NMPlatformIP4RtNextHop *extra_nexthops); int nm_platform_ip6_route_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformIP6Route *route); +int nm_platform_ip_nexthop_add(NMPlatform *self, + NMPNlmFlags flags, + const NMPObject *nexthop, + char **out_extack_msg); + GPtrArray *nm_platform_ip_route_get_prune_list(NMPlatform *self, int addr_family, int ifindex, @@ -2458,6 +2472,7 @@ gboolean nm_platform_ip_route_sync(NMPlatform *self, GPtrArray **out_routes_failed); gboolean nm_platform_ip_route_flush(NMPlatform *self, int addr_family, int ifindex); +gboolean nm_platform_ip_nexthop_flush(NMPlatform *self, int addr_family, int ifindex); int nm_platform_ip_route_get(NMPlatform *self, int addr_family, @@ -2466,6 +2481,17 @@ int nm_platform_ip_route_get(NMPlatform *self, int oif_ifindex, NMPObject **out_route); +GPtrArray *nm_platform_ip_nexthop_dump(NMPlatform *self, int addr_family, int ifindex); + +gboolean nm_platform_ip_nexthop_get(NMPlatform *self, guint32 nh_id, NMPObject **out_obj); + +gboolean nm_platform_ip_nexthop_sync(NMPlatform *self, + int addr_family, + int ifindex, + GPtrArray *known_nexthops, + GPtrArray *nexthops_prune, + GPtrArray *nexthops_platform); + int nm_platform_routing_rule_add(NMPlatform *self, NMPNlmFlags flags, const NMPlatformRoutingRule *routing_rule); @@ -2514,6 +2540,11 @@ nm_platform_ip4_route_to_string(const NMPlatformIP4Route *route, char *buf, gsiz } const char *nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsize len); +const char * +nm_platform_ip4_nexthop_to_string(const NMPlatformIP4NextHop *nexthop, char *buf, gsize len); +const char * +nm_platform_ip6_nexthop_to_string(const NMPlatformIP6NextHop *nexthop, char *buf, gsize len); + const char * nm_platform_routing_rule_to_string(const NMPlatformRoutingRule *routing_rule, char *buf, gsize len); const char *nm_platform_qdisc_to_string(const NMPlatformQdisc *qdisc, char *buf, gsize len); diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h index 3dec106a50..0bde881a6f 100644 --- a/src/libnm-platform/nmp-base.h +++ b/src/libnm-platform/nmp-base.h @@ -142,6 +142,8 @@ typedef struct _NMPlatformIP4Address NMPlatformIP4Address; typedef struct _NMPlatformIP4Route NMPlatformIP4Route; typedef struct _NMPlatformIP6Address NMPlatformIP6Address; typedef struct _NMPlatformIP6Route NMPlatformIP6Route; +typedef struct _NMPlatformIP4NextHop NMPlatformIP4NextHop; +typedef struct _NMPlatformIP6NextHop NMPlatformIP6NextHop; typedef struct _NMPlatformLink NMPlatformLink; typedef struct _NMPObject NMPObject; @@ -159,6 +161,11 @@ typedef enum _nm_packed { NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE, +#define NMP_OBJECT_TYPE_IP_NEXTHOP(is_ipv4) \ + ((is_ipv4) ? NMP_OBJECT_TYPE_IP4_NEXTHOP : NMP_OBJECT_TYPE_IP6_NEXTHOP) + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP, + NMP_OBJECT_TYPE_ROUTING_RULE, NMP_OBJECT_TYPE_QDISC, diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c index 56533c99c5..202d04b965 100644 --- a/src/libnm-platform/nmp-object.c +++ b/src/libnm-platform/nmp-object.c @@ -381,6 +381,8 @@ _idx_obj_part(const DedupMultiIdxType *idx_type, NMP_OBJECT_TYPE_IP6_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE, + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP, NMP_OBJECT_TYPE_QDISC, NMP_OBJECT_TYPE_TFILTER, NMP_OBJECT_TYPE_MPTCP_ADDR) @@ -899,6 +901,22 @@ nmp_object_stackinit_id_ip6_address(NMPObject *obj, int ifindex, const struct in return obj; } +const NMPObject * +nmp_object_stackinit_id_ip4_nexthop(NMPObject *obj, guint32 id) +{ + _nmp_object_stackinit_from_type(obj, NMP_OBJECT_TYPE_IP4_NEXTHOP); + obj->ip4_nexthop.id = id; + return obj; +} + +const NMPObject * +nmp_object_stackinit_id_ip6_nexthop(NMPObject *obj, guint32 id) +{ + _nmp_object_stackinit_from_type(obj, NMP_OBJECT_TYPE_IP6_NEXTHOP); + obj->ip6_nexthop.id = id; + return obj; +} + /*****************************************************************************/ const char * @@ -1668,6 +1686,22 @@ _vt_cmd_plobj_id_cmp_ip6_address(const NMPlatformObject *obj1, const NMPlatformO NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID); } +static int +_vt_cmd_plobj_id_cmp_ip4_nexthop(const NMPlatformObject *obj1, const NMPlatformObject *obj2) +{ + return nm_platform_ip4_nexthop_cmp((const NMPlatformIP4NextHop *) obj1, + (const NMPlatformIP4NextHop *) obj2, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID); +} + +static int +_vt_cmd_plobj_id_cmp_ip6_nexthop(const NMPlatformObject *obj1, const NMPlatformObject *obj2) +{ + return nm_platform_ip6_nexthop_cmp((const NMPlatformIP6NextHop *) obj1, + (const NMPlatformIP6NextHop *) obj2, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID); +} + _vt_cmd_plobj_id_cmp(qdisc, NMPlatformQdisc, { NM_CMP_FIELD(obj1, obj2, ifindex); NM_CMP_FIELD(obj1, obj2, parent); @@ -1770,6 +1804,14 @@ _vt_cmd_plobj_id_hash_update(ip6_route, NMPlatformIP6Route, { nm_platform_ip6_route_hash_update(obj, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID, h); }); +_vt_cmd_plobj_id_hash_update(ip4_nexthop, NMPlatformIP4NextHop, { + nm_platform_ip4_nexthop_hash_update(obj, NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID, h); +}); + +_vt_cmd_plobj_id_hash_update(ip6_nexthop, NMPlatformIP6NextHop, { + nm_platform_ip6_nexthop_hash_update(obj, NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID, h); +}); + _vt_cmd_plobj_id_hash_update(routing_rule, NMPlatformRoutingRule, { nm_platform_routing_rule_hash_update(obj, NM_PLATFORM_ROUTING_RULE_CMP_TYPE_ID, h); }); @@ -1804,6 +1846,38 @@ _vt_cmd_plobj_cmp_ip6_route(const NMPlatformObject *obj1, const NMPlatformObject NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL); } +static int +_vt_cmd_plobj_cmp_ip4_nexthop(const NMPlatformObject *obj1, const NMPlatformObject *obj2) +{ + return nm_platform_ip4_nexthop_cmp((const NMPlatformIP4NextHop *) obj1, + (const NMPlatformIP4NextHop *) obj2, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL); +} + +static void +_vt_cmd_plobj_hash_update_ip4_nexthop(const NMPlatformObject *obj, NMHashState *h) +{ + return nm_platform_ip4_nexthop_hash_update((const NMPlatformIP4NextHop *) obj, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL, + h); +} + +static int +_vt_cmd_plobj_cmp_ip6_nexthop(const NMPlatformObject *obj1, const NMPlatformObject *obj2) +{ + return nm_platform_ip6_nexthop_cmp((const NMPlatformIP6NextHop *) obj1, + (const NMPlatformIP6NextHop *) obj2, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL); +} + +static void +_vt_cmd_plobj_hash_update_ip6_nexthop(const NMPlatformObject *obj, NMHashState *h) +{ + return nm_platform_ip6_nexthop_hash_update((const NMPlatformIP6NextHop *) obj, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL, + h); +} + static void _vt_cmd_plobj_hash_update_routing_rule(const NMPlatformObject *obj, NMHashState *h) { @@ -2263,6 +2337,8 @@ nmp_lookup_init_obj_type(NMPLookup *lookup, NMPObjectType obj_type) case NMP_OBJECT_TYPE_IP6_ADDRESS: case NMP_OBJECT_TYPE_IP4_ROUTE: case NMP_OBJECT_TYPE_IP6_ROUTE: + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + case NMP_OBJECT_TYPE_IP6_NEXTHOP: case NMP_OBJECT_TYPE_ROUTING_RULE: case NMP_OBJECT_TYPE_QDISC: case NMP_OBJECT_TYPE_TFILTER: @@ -2300,6 +2376,8 @@ nmp_lookup_init_object_by_ifindex(NMPLookup *lookup, NMPObjectType obj_type, int NMP_OBJECT_TYPE_IP6_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE, + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP, NMP_OBJECT_TYPE_QDISC, NMP_OBJECT_TYPE_TFILTER, NMP_OBJECT_TYPE_MPTCP_ADDR)); @@ -3400,6 +3478,36 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = { .cmd_plobj_hash_update = _vt_cmd_plobj_hash_update_ip6_route, .cmd_plobj_cmp = _vt_cmd_plobj_cmp_ip6_route, }, + [NMP_OBJECT_TYPE_IP4_NEXTHOP - 1] = + { + .parent = DEDUP_MULTI_OBJ_CLASS_INIT(), + .obj_type = NMP_OBJECT_TYPE_IP4_NEXTHOP, + .sizeof_data = sizeof(NMPObjectIP4NextHop), + .sizeof_public = sizeof(NMPlatformIP4NextHop), + .obj_type_name = "ip4-nexthop", + .addr_family = AF_INET, + .cmd_plobj_id_cmp = _vt_cmd_plobj_id_cmp_ip4_nexthop, + .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip4_nexthop, + .cmd_plobj_to_string_id = (CmdPlobjToStringIdFunc) nm_platform_ip4_nexthop_to_string, + .cmd_plobj_to_string = (CmdPlobjToStringFunc) nm_platform_ip4_nexthop_to_string, + .cmd_plobj_hash_update = _vt_cmd_plobj_hash_update_ip4_nexthop, + .cmd_plobj_cmp = _vt_cmd_plobj_cmp_ip4_nexthop, + }, + [NMP_OBJECT_TYPE_IP6_NEXTHOP - 1] = + { + .parent = DEDUP_MULTI_OBJ_CLASS_INIT(), + .obj_type = NMP_OBJECT_TYPE_IP6_NEXTHOP, + .sizeof_data = sizeof(NMPObjectIP6NextHop), + .sizeof_public = sizeof(NMPlatformIP6NextHop), + .obj_type_name = "ip6-nexthop", + .addr_family = AF_INET6, + .cmd_plobj_id_cmp = _vt_cmd_plobj_id_cmp_ip6_nexthop, + .cmd_plobj_id_hash_update = _vt_cmd_plobj_id_hash_update_ip6_nexthop, + .cmd_plobj_to_string_id = (CmdPlobjToStringIdFunc) nm_platform_ip6_nexthop_to_string, + .cmd_plobj_to_string = (CmdPlobjToStringFunc) nm_platform_ip6_nexthop_to_string, + .cmd_plobj_hash_update = _vt_cmd_plobj_hash_update_ip6_nexthop, + .cmd_plobj_cmp = _vt_cmd_plobj_cmp_ip6_nexthop, + }, [NMP_OBJECT_TYPE_ROUTING_RULE - 1] = { .parent = DEDUP_MULTI_OBJ_CLASS_INIT(), diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h index b526fc3bec..b8312f67bf 100644 --- a/src/libnm-platform/nmp-object.h +++ b/src/libnm-platform/nmp-object.h @@ -348,6 +348,14 @@ typedef struct { NMPlatformIP6Route _public; } NMPObjectIP6Route; +typedef struct { + NMPlatformIP4NextHop _public; +} NMPObjectIP4NextHop; + +typedef struct { + NMPlatformIP6NextHop _public; +} NMPObjectIP6NextHop; + typedef struct { NMPlatformRoutingRule _public; } NMPObjectRoutingRule; @@ -445,6 +453,13 @@ struct _NMPObject { NMPObjectIP4Route _ip4_route; NMPObjectIP6Route _ip6_route; + NMPlatformIPNextHop ip_nexthop; + NMPlatformIPXNextHop ipx_nexthop; + NMPlatformIP4NextHop ip4_nexthop; + NMPlatformIP6NextHop ip6_nexthop; + NMPObjectIP4NextHop _ip4_nexthop; + NMPObjectIP6NextHop _ip6_nexthop; + NMPlatformRoutingRule routing_rule; NMPObjectRoutingRule _routing_rule; @@ -536,6 +551,8 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type) case NMP_OBJECT_TYPE_IP6_ADDRESS: case NMP_OBJECT_TYPE_IP4_ROUTE: case NMP_OBJECT_TYPE_IP6_ROUTE: + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + case NMP_OBJECT_TYPE_IP6_NEXTHOP: case NMP_OBJECT_TYPE_QDISC: @@ -620,6 +637,12 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type) _NMP_OBJECT_CAST(obj, ipx_route, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE) #define NMP_OBJECT_CAST_IP4_ROUTE(obj) _NMP_OBJECT_CAST(obj, ip4_route, NMP_OBJECT_TYPE_IP4_ROUTE) #define NMP_OBJECT_CAST_IP6_ROUTE(obj) _NMP_OBJECT_CAST(obj, ip6_route, NMP_OBJECT_TYPE_IP6_ROUTE) +#define NMP_OBJECT_CAST_IP4_NEXTHOP(obj) \ + _NMP_OBJECT_CAST(obj, ip4_nexthop, NMP_OBJECT_TYPE_IP4_NEXTHOP) +#define NMP_OBJECT_CAST_IP6_NEXTHOP(obj) \ + _NMP_OBJECT_CAST(obj, ip6_nexthop, NMP_OBJECT_TYPE_IP6_NEXTHOP) +#define NMP_OBJECT_CAST_IP_NEXTHOP(obj) \ + _NMP_OBJECT_CAST(obj, ip_nexthop, NMP_OBJECT_TYPE_IP4_NEXTHOP, NMP_OBJECT_TYPE_IP6_NEXTHOP) #define NMP_OBJECT_CAST_ROUTING_RULE(obj) \ _NMP_OBJECT_CAST(obj, routing_rule, NMP_OBJECT_TYPE_ROUTING_RULE) #define NMP_OBJECT_CAST_QDISC(obj) _NMP_OBJECT_CAST(obj, qdisc, NMP_OBJECT_TYPE_QDISC) @@ -759,6 +782,9 @@ const NMPObject *nmp_object_stackinit_id_ip4_address(NMPObject *obj, const NMPObject * nmp_object_stackinit_id_ip6_address(NMPObject *obj, int ifindex, const struct in6_addr *address); +const NMPObject *nmp_object_stackinit_id_ip4_nexthop(NMPObject *obj, guint32 id); +const NMPObject *nmp_object_stackinit_id_ip6_nexthop(NMPObject *obj, guint32 id); + const char *nmp_object_to_string(const NMPObject *obj, NMPObjectToStringMode to_string_mode, char *buf, diff --git a/src/libnm-platform/nmp-plobj.c b/src/libnm-platform/nmp-plobj.c index 70664634e6..c060ff90a0 100644 --- a/src/libnm-platform/nmp-plobj.c +++ b/src/libnm-platform/nmp-plobj.c @@ -170,6 +170,106 @@ nm_platform_ip4_address_cmp(const NMPlatformIP4Address *a, /*****************************************************************************/ +void +nm_platform_ip4_nexthop_hash_update(const NMPlatformIP4NextHop *obj, + NMPlatformIPNextHopCmpType cmp_type, + NMHashState *h) +{ + switch (cmp_type) { + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID: + nm_hash_update_vals(h, obj->id); + break; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY: + nm_hash_update_vals(h, + obj->id, + obj->ifindex, + nmp_utils_ip_config_source_round_trip_rtprot(obj->nh_source), + obj->gateway); + break; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL: + nm_hash_update_vals(h, obj->id, obj->ifindex, obj->nh_source, obj->gateway); + break; + } +} + +int +nm_platform_ip4_nexthop_cmp(const NMPlatformIP4NextHop *a, + const NMPlatformIP4NextHop *b, + NMPlatformIPNextHopCmpType cmp_type) +{ + NM_CMP_SELF(a, b); + + NM_CMP_FIELD(a, b, id); + + switch (cmp_type) { + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID: + return 0; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY: + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->nh_source), + nmp_utils_ip_config_source_round_trip_rtprot(b->nh_source)); + NM_CMP_FIELD(a, b, gateway); + return 0; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL: + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_FIELD(a, b, nh_source); + NM_CMP_FIELD(a, b, gateway); + return 0; + } + return nm_assert_unreachable_val(0); +} + +void +nm_platform_ip6_nexthop_hash_update(const NMPlatformIP6NextHop *obj, + NMPlatformIPNextHopCmpType cmp_type, + NMHashState *h) +{ + switch (cmp_type) { + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID: + nm_hash_update_vals(h, obj->id); + break; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY: + nm_hash_update_vals(h, + obj->id, + obj->ifindex, + nmp_utils_ip_config_source_round_trip_rtprot(obj->nh_source), + obj->gateway); + break; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL: + nm_hash_update_vals(h, obj->id, obj->ifindex, obj->nh_source, obj->gateway); + break; + } +} + +int +nm_platform_ip6_nexthop_cmp(const NMPlatformIP6NextHop *a, + const NMPlatformIP6NextHop *b, + NMPlatformIPNextHopCmpType cmp_type) +{ + NM_CMP_SELF(a, b); + + NM_CMP_FIELD(a, b, id); + + switch (cmp_type) { + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID: + return 0; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY: + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_DIRECT(nmp_utils_ip_config_source_round_trip_rtprot(a->nh_source), + nmp_utils_ip_config_source_round_trip_rtprot(b->nh_source)); + NM_CMP_FIELD_IN6ADDR(a, b, gateway); + return 0; + case NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL: + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_FIELD(a, b, nh_source); + NM_CMP_FIELD_IN6ADDR(a, b, gateway); + return 0; + } + return nm_assert_unreachable_val(0); +} + +/*****************************************************************************/ + void nm_platform_ip6_address_hash_update(const NMPlatformIP6Address *obj, NMHashState *h) { diff --git a/src/libnm-platform/nmp-plobj.h b/src/libnm-platform/nmp-plobj.h index 7760c02c19..bb7a1d2f2f 100644 --- a/src/libnm-platform/nmp-plobj.h +++ b/src/libnm-platform/nmp-plobj.h @@ -141,6 +141,34 @@ typedef union { /*****************************************************************************/ +typedef struct { + __NMPlatformObjWithIfindex_COMMON; + guint32 id; + NMIPConfigSource nh_source; +} _nm_alignas(NMPlatformObject) NMPlatformIPNextHop; + +struct _NMPlatformIP4NextHop { + __NMPlatformObjWithIfindex_COMMON; + guint32 id; + NMIPConfigSource nh_source; + in_addr_t gateway; +} _nm_alignas(NMPlatformObject); + +struct _NMPlatformIP6NextHop { + __NMPlatformObjWithIfindex_COMMON; + guint32 id; + NMIPConfigSource nh_source; + struct in6_addr gateway; +} _nm_alignas(NMPlatformObject); + +typedef union { + NMPlatformIPNextHop nhx; + NMPlatformIP4NextHop nh4; + NMPlatformIP6NextHop nh6; +} NMPlatformIPXNextHop; + +/*****************************************************************************/ + typedef enum { NM_PLATFORM_IP_ADDRESS_CMP_TYPE_ID, @@ -162,6 +190,14 @@ typedef enum { /*****************************************************************************/ +typedef enum { + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_ID, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_SEMANTICALLY, + NM_PLATFORM_IP_NEXTHOP_CMP_TYPE_FULL, +} NMPlatformIPNextHopCmpType; + +/*****************************************************************************/ + typedef struct { bool is_ip4; NMPObjectType obj_type; @@ -299,6 +335,22 @@ gboolean nm_platform_ip_address_match(int addr_family, const NMPlatformIPAddress *addr, NMPlatformMatchFlags match_flag); +void nm_platform_ip4_nexthop_hash_update(const NMPlatformIP4NextHop *obj, + NMPlatformIPNextHopCmpType cmp_type, + NMHashState *h); + +int nm_platform_ip4_nexthop_cmp(const NMPlatformIP4NextHop *a, + const NMPlatformIP4NextHop *b, + NMPlatformIPNextHopCmpType cmp_type); + +void nm_platform_ip6_nexthop_hash_update(const NMPlatformIP6NextHop *obj, + NMPlatformIPNextHopCmpType cmp_type, + NMHashState *h); + +int nm_platform_ip6_nexthop_cmp(const NMPlatformIP6NextHop *a, + const NMPlatformIP6NextHop *b, + NMPlatformIPNextHopCmpType cmp_type); + /*****************************************************************************/ #endif /* __NMP_PLOBJ_H__ */ From f6967057277f20bb7be174ca822b1cd473d8e83a Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:15:57 +0100 Subject: [PATCH 3/9] platform: support setting the nexthop in routes Routes can now reference a nexthop id instead of the (ifindex, gateway) tuple. When sending a route with a nexthop to the kernel, the ifindex and gateway must be unset, otherwise the route will be rejected. When the kernel sends notifications to userspace it copies the ifindex and the gateway from the nexthop into the route. In a route platform object, the ifindex and gateway are ignored if the routes has a nexthop. The ifindex must be always set when creating new synthetic route because the platform code needs it to properly track the object. This is implemented only for IPv6 routes, because it will be needed by the ndisc code. --- src/core/platform/tests/test-route.c | 77 ++++++++++++++++++++++++++ src/libnm-platform/nm-linux-platform.c | 31 +++++++++-- src/libnm-platform/nm-platform.c | 77 +++++++++++++++----------- src/libnm-platform/nm-platform.h | 16 ++++++ 4 files changed, 165 insertions(+), 36 deletions(-) diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c index 78301ffec6..5baceb51f8 100644 --- a/src/core/platform/tests/test-route.c +++ b/src/core/platform/tests/test-route.c @@ -2408,6 +2408,82 @@ test_nexthop_add(void) /*****************************************************************************/ +static void +test_ip6_route_with_nexthop(void) +{ + const int ifindex = NMTSTP_ENV1_IFINDEXES[0]; + NMPObject obj; + int r; + const NMPlatformIP6Route *r6; + struct in6_addr network; + const guint32 metric = 22987; + + g_assert(nm_platform_ip6_address_add(NM_PLATFORM_GET, + ifindex, + nmtst_inet6_from_string("fe80::1"), + 64, + in6addr_any, + NM_PLATFORM_LIFETIME_PERMANENT, + NM_PLATFORM_LIFETIME_PERMANENT, + 0, + NULL)); + + /* Add IPv6 nexthop without gateway (id 300) */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_NEXTHOP, NULL); + obj.ip6_nexthop.id = 300; + obj.ip6_nexthop.ifindex = ifindex; + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Add IPv6 nexthop with gateway (id 301) */ + nmp_object_stackinit(&obj, NMP_OBJECT_TYPE_IP6_NEXTHOP, NULL); + obj.ip6_nexthop.id = 301; + obj.ip6_nexthop.ifindex = ifindex; + obj.ip6_nexthop.gateway = nmtst_inet6_from_string("fe80::99"); + r = nm_platform_ip_nexthop_add(NM_PLATFORM_GET, NMP_NLM_FLAG_ADD, &obj, NULL); + g_assert_cmpint(r, ==, 0); + + /* Add route using nexthop without gateway */ + inet_pton(AF_INET6, "a:b:c:1::", &network); + r = nm_platform_ip6_route_add(NM_PLATFORM_GET, + NMP_NLM_FLAG_REPLACE, + &((NMPlatformIP6Route) { + .ifindex = ifindex, + .network = network, + .plen = 64, + .metric = metric, + .nhid = 300, + })); + g_assert_cmpint(r, ==, 0); + + r6 = nmtstp_ip6_route_get(NM_PLATFORM_GET, ifindex, &network, 64, metric, NULL, 0); + g_assert(r6); + g_assert_cmpint(r6->ifindex, ==, ifindex); + g_assert_cmpint(r6->nhid, ==, 300); + nmtst_assert_ip6_address(&r6->gateway, "::"); + + /* Add route using nexthop with gateway */ + inet_pton(AF_INET6, "a:b:c:2::", &network); + r = nm_platform_ip6_route_add(NM_PLATFORM_GET, + NMP_NLM_FLAG_REPLACE, + &((NMPlatformIP6Route) { + .ifindex = ifindex, + .network = network, + .plen = 64, + .metric = metric, + .nhid = 301, + })); + g_assert_cmpint(r, ==, 0); + + r6 = nmtstp_ip6_route_get(NM_PLATFORM_GET, ifindex, &network, 64, metric, NULL, 0); + g_assert(r6); + g_assert_cmpint(r6->ifindex, ==, ifindex); + g_assert_cmpint(r6->nhid, ==, 301); + nmtst_assert_ip6_address(&r6->gateway, "fe80::99"); +} + +/*****************************************************************************/ + static void _ensure_onlink_routes(void) { @@ -2686,6 +2762,7 @@ _nmtstp_setup_tests(void) if (nmtstp_is_root_test()) { add_test_func_with_if2("/route/nexthop/dump", test_nexthop_dump); add_test_func("/route/nexthop/add", test_nexthop_add); + add_test_func("/route/ip6_with_nexthop", test_ip6_route_with_nexthop); } if (nmtstp_is_root_test()) { diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c index 95200182ec..9fb69a0378 100644 --- a/src/libnm-platform/nm-linux-platform.c +++ b/src/libnm-platform/nm-linux-platform.c @@ -4015,6 +4015,7 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter [RTA_PRIORITY] = {.type = NLA_U32}, [RTA_PREF] = {.type = NLA_U8}, [RTA_FLOW] = {.type = NLA_U32}, + [RTA_NH_ID] = {.type = NLA_U32}, [RTA_CACHEINFO] = {.minlen = nm_offsetofend(struct rta_cacheinfo, rta_tsage)}, [RTA_VIA] = {.minlen = nm_offsetofend(struct rtvia, rtvia_family)}, [RTA_METRICS] = {.type = NLA_NESTED}, @@ -4052,6 +4053,7 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter guint32 mtu = 0; guint32 rto_min = 0; guint32 lock = 0; + guint32 nhid = 0; gboolean quickack = FALSE; gboolean rto_min_set = FALSE; @@ -4097,7 +4099,12 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter if (rtm->rtm_dst_len > (IS_IPv4 ? 32 : 128)) return NULL; - if (tb[RTA_MULTIPATH]) { + if (tb[RTA_NH_ID] && !IS_IPv4) { + /* we only support NHID for IPv6 at the moment */ + nhid = nla_get_u32(tb[RTA_NH_ID]); + } + + if (nhid == 0 && tb[RTA_MULTIPATH]) { size_t tlen; struct rtnexthop *rtnh; guint idx; @@ -4206,7 +4213,7 @@ rta_multipath_done: return nm_assert_unreachable_val(NULL); } - if (tb[RTA_OIF] || tb[RTA_GATEWAY] || tb[RTA_FLOW] || tb[RTA_VIA]) { + if (nhid == 0 && (tb[RTA_OIF] || tb[RTA_GATEWAY] || tb[RTA_FLOW] || tb[RTA_VIA])) { int ifindex = 0; NMIPAddr gateway = {}; @@ -4252,6 +4259,15 @@ rta_multipath_done: } } + if (nhid != 0) { + if (tb[RTA_OIF]) { + nh.ifindex = nla_get_u32(tb[RTA_OIF]); + nh.found = TRUE; + } + if (_check_addr_or_return_null(tb, RTA_GATEWAY, addr_len)) + memcpy(&nh.gateway, nla_data(tb[RTA_GATEWAY]), addr_len); + } + if (nm_platform_route_type_is_nodev(rtm->rtm_type)) { /* These routes are special. They don't have an device/ifindex. * @@ -4388,6 +4404,9 @@ rta_multipath_done: obj->ip6_route.src_plen = rtm->rtm_src_len; } + if (!IS_IPv4) + obj->ip6_route.nhid = nhid; + obj->ip_route.mss = mss; obj->ip_route.window = window; obj->ip_route.cwnd = cwnd; @@ -5991,10 +6010,14 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob NLA_PUT(msg, RTA_GATEWAY, addr_len, &obj->ip4_route.gateway); } } else { - if (!IN6_IS_ADDR_UNSPECIFIED(&obj->ip6_route.gateway)) + if (obj->ip6_route.nhid != 0) { + NLA_PUT_U32(msg, RTA_NH_ID, obj->ip6_route.nhid); + } else if (!IN6_IS_ADDR_UNSPECIFIED(&obj->ip6_route.gateway)) NLA_PUT(msg, RTA_GATEWAY, addr_len, &obj->ip6_route.gateway); } - NLA_PUT_U32(msg, RTA_OIF, obj->ip_route.ifindex); + + if (IS_IPv4 || obj->ip6_route.nhid == 0) + NLA_PUT_U32(msg, RTA_OIF, obj->ip_route.ifindex); if (!IS_IPv4 && obj->ip6_route.rt_pref != NM_ICMPV6_ROUTER_PREF_MEDIUM) NLA_PUT_U8(msg, RTA_PREF, obj->ip6_route.rt_pref); diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c index 8ecadea16c..857123d8cd 100644 --- a/src/libnm-platform/nm-platform.c +++ b/src/libnm-platform/nm-platform.c @@ -7612,33 +7612,37 @@ nm_platform_ip6_nexthop_to_string(const NMPlatformIP6NextHop *nexthop, char *buf const char * nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsize len) { - char s_network[INET6_ADDRSTRLEN]; - char s_gateway[INET6_ADDRSTRLEN]; - char s_pref_src[INET6_ADDRSTRLEN]; - char s_src_all[INET6_ADDRSTRLEN + 40]; - char s_src[INET6_ADDRSTRLEN]; - char str_type[30]; - char str_table[30]; - char str_pref[40]; - char str_pref2[30]; - char str_dev[30]; - char str_mss[32]; - char s_source[50]; - char str_window[32]; - char str_cwnd[32]; - char str_initcwnd[32]; - char str_initrwnd[32]; - char str_rto_min[32]; - char str_mtu[32]; - char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN]; - char str_metric[30]; + char s_network[INET6_ADDRSTRLEN]; + char s_gateway[INET6_ADDRSTRLEN]; + char s_pref_src[INET6_ADDRSTRLEN]; + char s_src_all[INET6_ADDRSTRLEN + 40]; + char s_src[INET6_ADDRSTRLEN]; + char str_type[30]; + char str_table[30]; + char str_pref[40]; + char str_pref2[30]; + char str_dev[30]; + char str_mss[32]; + char s_source[50]; + char str_window[32]; + char str_cwnd[32]; + char str_initcwnd[32]; + char str_initrwnd[32]; + char str_rto_min[32]; + char str_mtu[32]; + char str_rtm_flags[_RTM_FLAGS_TO_STRING_MAXLEN]; + char str_metric[30]; + gboolean has_nhid = FALSE; if (!nm_utils_to_string_buffer_init_null(route, &buf, &len)) return buf; inet_ntop(AF_INET6, &route->network, s_network, sizeof(s_network)); - if (IN6_IS_ADDR_UNSPECIFIED(&route->gateway)) + if (route->nhid) { + g_snprintf(s_gateway, sizeof(s_gateway), " nhid %u", route->nhid); + has_nhid = TRUE; + } else if (IN6_IS_ADDR_UNSPECIFIED(&route->gateway)) s_gateway[0] = '\0'; else inet_ntop(AF_INET6, &route->gateway, s_gateway, sizeof(s_gateway)); @@ -7682,7 +7686,7 @@ nm_platform_ip6_route_to_string(const NMPlatformIP6Route *route, char *buf, gsiz : ""), s_network, route->plen, - s_gateway[0] ? " via " : "", + !has_nhid && s_gateway[0] ? " via " : "", s_gateway, _to_string_dev(str_dev, route->ifindex), route->metric_any @@ -9459,23 +9463,25 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj, *nm_ip6_addr_clear_host_address(&a1, &obj->network, obj->plen), obj->plen, obj->metric, + obj->nhid, *nm_ip6_addr_clear_host_address(&a2, &obj->src, obj->src_plen), obj->src_plen, NM_HASH_COMBINE_BOOLS(guint8, obj->metric_any, obj->table_any), /* on top of WEAK_ID: */ - obj->ifindex, - obj->gateway); + obj->nhid == 0 ? obj->ifindex : 0, + obj->nhid == 0 ? obj->gateway : in6addr_any); break; case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY: nm_hash_update_vals( h, obj->type_coerced, nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(obj)), - obj->ifindex, *nm_ip6_addr_clear_host_address(&a1, &obj->network, obj->plen), obj->plen, obj->metric, - obj->gateway, + obj->nhid, + obj->nhid == 0 ? obj->ifindex : 0, + obj->nhid == 0 ? obj->gateway : in6addr_any, obj->pref_src, *nm_ip6_addr_clear_host_address(&a2, &obj->src, obj->src_plen), obj->src_plen, @@ -9504,10 +9510,11 @@ nm_platform_ip6_route_hash_update(const NMPlatformIP6Route *obj, nm_hash_update_vals(h, obj->type_coerced, obj->table_coerced, - obj->ifindex, obj->network, obj->metric, - obj->gateway, + obj->nhid, + obj->nhid == 0 ? obj->ifindex : 0, + obj->nhid == 0 ? obj->gateway : in6addr_any, obj->pref_src, obj->src, obj->src_plen, @@ -9557,9 +9564,12 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a, NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, NM_MIN(a->src_plen, b->src_plen)); NM_CMP_FIELD(a, b, src_plen); if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID) { - NM_CMP_FIELD(a, b, ifindex); NM_CMP_FIELD(a, b, type_coerced); - NM_CMP_FIELD_IN6ADDR(a, b, gateway); + NM_CMP_FIELD(a, b, nhid); + if (a->nhid == 0) { + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_FIELD_IN6ADDR(a, b, gateway); + } } break; case NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY: @@ -9571,7 +9581,6 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a, nm_platform_ip_route_get_effective_table(NM_PLATFORM_IP_ROUTE_CAST(b))); } else NM_CMP_FIELD(a, b, table_coerced); - NM_CMP_FIELD(a, b, ifindex); if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->network, &b->network, NM_MIN(a->plen, b->plen)); else @@ -9579,7 +9588,11 @@ nm_platform_ip6_route_cmp(const NMPlatformIP6Route *a, NM_CMP_FIELD(a, b, plen); NM_CMP_FIELD_UNSAFE(a, b, metric_any); NM_CMP_FIELD(a, b, metric); - NM_CMP_FIELD_IN6ADDR(a, b, gateway); + NM_CMP_FIELD(a, b, nhid); + if (a->nhid == 0) { + NM_CMP_FIELD(a, b, ifindex); + NM_CMP_FIELD_IN6ADDR(a, b, gateway); + } NM_CMP_FIELD_IN6ADDR(a, b, pref_src); if (cmp_type == NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY) { NM_CMP_DIRECT_IP6_ADDR_SAME_PREFIX(&a->src, &b->src, NM_MIN(a->src_plen, b->src_plen)); diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h index 338e621642..16bb920c5f 100644 --- a/src/libnm-platform/nm-platform.h +++ b/src/libnm-platform/nm-platform.h @@ -531,6 +531,22 @@ struct _NMPlatformIP6Route { * The type is guint8 to keep the struct size small. But the values are compatible with * the NMIcmpv6RouterPref enum. */ guint8 rt_pref; + + /* RTA_NH_ID. The unique id of the nexthop object. + * + * When sending a route with a nexthop to the kernel, the ifindex + * and gateway must be unset, otherwise the route will be + * rejected. When the kernel sends notifications to userspace it + * copies the ifindex and the gateway from the nexthop into the + * route. + * + * In a route platform object, the ifindex and gateway are ignored + * if the routes has a nexthop. However, in the platform cache, routes + * with nexthops always have also the ifindex and the gateway set. In + * particular, the ifindex must be set when creating new synthetic + * route because the platform code needs it to properly track the object. + */ + guint32 nhid; } _nm_alignas(NMPlatformObject); typedef union { From 97de69515dc91f5d2c0222333e1871b175262f0e Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:18:00 +0100 Subject: [PATCH 4/9] core: support nexthops in l3cd Track a list of nexthops in NML3ConfigData. --- src/core/nm-l3-config-data.c | 126 ++++++++++++++++++++++++++++++++++- src/core/nm-l3-config-data.h | 17 +++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c index 0b2804fa72..ad4c408a25 100644 --- a/src/core/nm-l3-config-data.c +++ b/src/core/nm-l3-config-data.c @@ -42,6 +42,14 @@ struct _NML3ConfigData { DedupMultiIdxType idx_routes_x[2]; }; + union { + struct { + DedupMultiIdxType idx_nexthops_6; + DedupMultiIdxType idx_nexthops_4; + }; + DedupMultiIdxType idx_nexthops_x[2]; + }; + union { struct { const NMPObject *best_default_route_6; @@ -455,6 +463,17 @@ nm_l3_config_data_log(const NML3ConfigData *self, i++; } + nm_l3_config_data_iter_obj_for_each (&iter, + self, + &obj, + NMP_OBJECT_TYPE_IP_NEXTHOP(IS_IPv4)) { + _L("nexthop%c[%u]: %s", + nm_utils_addr_family_to_char(addr_family), + i, + nmp_object_to_string(obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf))); + i++; + } + if (self->route_table_sync_x[IS_IPv4] != NM_IP_ROUTE_TABLE_SYNC_MODE_NONE) { _L("route-table-sync-mode%c: %d", nm_utils_addr_family_to_char(addr_family), @@ -777,6 +796,8 @@ nm_l3_config_data_new(NMDedupMultiIndex *multi_idx, int ifindex, NMIPConfigSourc _idx_type_init(&self->idx_addresses_6, NMP_OBJECT_TYPE_IP6_ADDRESS); _idx_type_init(&self->idx_routes_4, NMP_OBJECT_TYPE_IP4_ROUTE); _idx_type_init(&self->idx_routes_6, NMP_OBJECT_TYPE_IP6_ROUTE); + _idx_type_init(&self->idx_nexthops_4, NMP_OBJECT_TYPE_IP4_NEXTHOP); + _idx_type_init(&self->idx_nexthops_6, NMP_OBJECT_TYPE_IP6_NEXTHOP); return self; } @@ -844,6 +865,8 @@ nm_l3_config_data_unref(const NML3ConfigData *self) nm_dedup_multi_index_remove_idx(mutable->multi_idx, &mutable->idx_addresses_6.parent); nm_dedup_multi_index_remove_idx(mutable->multi_idx, &mutable->idx_routes_4.parent); nm_dedup_multi_index_remove_idx(mutable->multi_idx, &mutable->idx_routes_6.parent); + nm_dedup_multi_index_remove_idx(mutable->multi_idx, &mutable->idx_nexthops_4.parent); + nm_dedup_multi_index_remove_idx(mutable->multi_idx, &mutable->idx_nexthops_6.parent); nmp_object_unref(mutable->best_default_route_4); nmp_object_unref(mutable->best_default_route_6); @@ -938,6 +961,40 @@ nm_l3_config_data_lookup_route(const NML3ConfigData *self, nmp_object_stackinit(&obj_stack, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), needle)); } +const NMPlatformIP4NextHop * +nm_l3_config_data_lookup_nexthop4(const NML3ConfigData *self, guint32 id) +{ + const NMDedupMultiEntry *head; + NMPObject obj_stack; + + nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE)); + + nmp_object_stackinit_id_ip4_nexthop(&obj_stack, id); + + head = nm_l3_config_data_lookup_obj(self, &obj_stack); + if (!head) + return NULL; + + return NMP_OBJECT_CAST_IP4_NEXTHOP(head->obj); +} + +const NMPlatformIP6NextHop * +nm_l3_config_data_lookup_nexthop6(const NML3ConfigData *self, guint32 id) +{ + const NMDedupMultiEntry *head; + NMPObject obj_stack; + + nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE)); + + nmp_object_stackinit_id_ip6_nexthop(&obj_stack, id); + + head = nm_l3_config_data_lookup_obj(self, &obj_stack); + if (!head) + return NULL; + + return NMP_OBJECT_CAST_IP6_NEXTHOP(head->obj); +} + const NMDedupMultiIdxType * nm_l3_config_data_lookup_index(const NML3ConfigData *self, NMPObjectType obj_type) { @@ -952,6 +1009,10 @@ nm_l3_config_data_lookup_index(const NML3ConfigData *self, NMPObjectType obj_typ return &self->idx_routes_4.parent; case NMP_OBJECT_TYPE_IP6_ROUTE: return &self->idx_routes_6.parent; + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + return &self->idx_nexthops_4.parent; + case NMP_OBJECT_TYPE_IP6_NEXTHOP: + return &self->idx_nexthops_6.parent; default: return nm_assert_unreachable_val(NULL); } @@ -1168,7 +1229,9 @@ _l3_config_data_add_obj(NMDedupMultiIndex *multi_idx, NMP_OBJECT_TYPE_IP4_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ADDRESS, - NMP_OBJECT_TYPE_IP6_ROUTE)); + NMP_OBJECT_TYPE_IP6_ROUTE, + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP)); nm_assert((!!obj_new) != (!!pl_new)); if (NM_IN_SET(idx_type->obj_type, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE)) { @@ -1257,6 +1320,9 @@ _l3_config_data_add_obj(NMDedupMultiIndex *multi_idx, modified = TRUE; } + break; + case NMP_OBJECT_TYPE_IP4_NEXTHOP: + case NMP_OBJECT_TYPE_IP6_NEXTHOP: break; default: nm_assert_not_reached(); @@ -1448,6 +1514,29 @@ nm_l3_config_data_add_route_full(NML3ConfigData *self, return changed; } +gboolean +nm_l3_config_data_add_nexthop(NML3ConfigData *self, + int addr_family, + const NMPObject *obj_new, + const NMPlatformIPNextHop *pl_new) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + + nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE)); + nm_assert_addr_family(addr_family); + nm_assert((!pl_new) != (!obj_new)); + nm_assert(!obj_new || (NMP_OBJECT_GET_ADDR_FAMILY(obj_new) == addr_family)); + + return _l3_config_data_add_obj(self->multi_idx, + &self->idx_nexthops_x[IS_IPv4], + self->ifindex, + obj_new, + (const NMPlatformObject *) pl_new, + NM_L3_CONFIG_ADD_FLAGS_MERGE, + NULL, + NULL); +} + const NMPObject * nm_l3_config_data_get_best_default_route(const NML3ConfigData *self, int addr_family) { @@ -3673,6 +3762,41 @@ nm_l3_config_data_merge(NML3ConfigData *self, #undef _ensure_r } + nm_l3_config_data_iter_obj_for_each (&iter, + src, + &obj, + NMP_OBJECT_TYPE_IP_NEXTHOP(IS_IPv4)) { + const NMPlatformIPNextHop *nh_src = NMP_OBJECT_CAST_IP_NEXTHOP(obj); + NMPlatformIPXNextHop nh; +#define _ensure_a() \ + G_STMT_START \ + { \ + if (nh_src != &nh.nhx) { \ + if (IS_IPv4) \ + nh.nh4 = *NMP_OBJECT_CAST_IP4_NEXTHOP(obj); \ + else \ + nh.nh6 = *NMP_OBJECT_CAST_IP6_NEXTHOP(obj); \ + nh_src = &nh.nhx; \ + } \ + } \ + G_STMT_END + + if (nh_src->ifindex != self->ifindex) { + _ensure_a(); + nh.nhx.ifindex = self->ifindex; + } + + _l3_config_data_add_obj(self->multi_idx, + &self->idx_nexthops_x[IS_IPv4], + self->ifindex, + nh_src == &nh.nhx ? NULL : obj, + (const NMPlatformObject *) (nh_src == &nh.nhx ? nh_src : NULL), + NM_L3_CONFIG_ADD_FLAGS_EXCLUSIVE, + NULL, + NULL); +#undef _ensure_r + } + if (!NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_NO_DNS)) _strv_ptrarray_merge(&self->nameservers_x[IS_IPv4], src->nameservers_x[IS_IPv4]); diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h index b76e11f9b1..5098c4f767 100644 --- a/src/core/nm-l3-config-data.h +++ b/src/core/nm-l3-config-data.h @@ -251,6 +251,12 @@ const NMDedupMultiEntry *nm_l3_config_data_lookup_route(const NML3ConfigData int addr_family, const NMPlatformIPRoute *needle); +const NMPlatformIP4NextHop *nm_l3_config_data_lookup_nexthop4(const NML3ConfigData *self, + guint32 id); + +const NMPlatformIP6NextHop *nm_l3_config_data_lookup_nexthop6(const NML3ConfigData *self, + guint32 id); + const NMDedupMultiHeadEntry *nm_l3_config_data_lookup_objs(const NML3ConfigData *self, NMPObjectType obj_type); @@ -266,6 +272,12 @@ nm_l3_config_data_lookup_routes(const NML3ConfigData *self, int addr_family) return nm_l3_config_data_lookup_objs(self, NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family))); } +static inline const NMDedupMultiHeadEntry * +nm_l3_config_data_lookup_nexthops(const NML3ConfigData *self, int addr_family) +{ + return nm_l3_config_data_lookup_objs(self, NMP_OBJECT_TYPE_IP_NEXTHOP(NM_IS_IPv4(addr_family))); +} + #define nm_l3_config_data_iter_obj_for_each(iter, self, obj, type) \ for (nm_dedup_multi_iter_init((iter), nm_l3_config_data_lookup_objs((self), (type))); \ nm_platform_dedup_multi_iter_next_obj((iter), (obj), (type));) @@ -435,6 +447,11 @@ nm_l3_config_data_add_route(NML3ConfigData *self, NULL); } +gboolean nm_l3_config_data_add_nexthop(NML3ConfigData *self, + int addr_family, + const NMPObject *obj_new, + const NMPlatformIPNextHop *pl_new); + static inline gboolean nm_l3_config_data_add_route_4(NML3ConfigData *self, const NMPlatformIP4Route *rt) { From bc0827fd65aa1574308d746775274ae00ab4c7e3 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:18:41 +0100 Subject: [PATCH 5/9] core: sync nexthops in l3cfg When committing the configuration in the NML3Cfg, also sync the nexthops. Since we don't track them in the platform cache, we can't use the existing "zombie" mechanism to detect which objects were added by us and are no longer needed. For that, we need to keep track explicitly of what nexthop we added. --- src/core/nm-l3cfg.c | 102 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c index 59fc6e4d7f..5f60f86b03 100644 --- a/src/core/nm-l3cfg.c +++ b/src/core/nm-l3cfg.c @@ -319,6 +319,17 @@ typedef struct _NML3CfgPrivate { int clat_socket; #endif /* HAVE_CLAT */ + /* Tracks nexthop IDs that NM has configured, per address family. + * Used to build the nexthop prune list without relying on the + * platform cache or the zombie mechanism. */ + union { + struct { + GHashTable *nexthop_configured_ids_6; + GHashTable *nexthop_configured_ids_4; + }; + GHashTable *nexthop_configured_ids_x[2]; + }; + /* Whether we earlier configured MPTCP endpoints for the interface. */ union { struct { @@ -826,7 +837,9 @@ _nm_n_acd_data_probe_new(NML3Cfg *self, in_addr_t addr, guint32 timeout_msec, gp NMP_OBJECT_TYPE_IP4_ADDRESS, \ NMP_OBJECT_TYPE_IP6_ADDRESS, \ NMP_OBJECT_TYPE_IP4_ROUTE, \ - NMP_OBJECT_TYPE_IP6_ROUTE)); \ + NMP_OBJECT_TYPE_IP6_ROUTE, \ + NMP_OBJECT_TYPE_IP4_NEXTHOP, \ + NMP_OBJECT_TYPE_IP6_NEXTHOP)); \ nm_assert(!_obj_state->os_plobj || _obj_state->os_was_in_platform); \ nm_assert(_obj_state->os_failedobj_expiry_msec != 0 \ || _obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL); \ @@ -1189,6 +1202,8 @@ _obj_states_update_all(NML3Cfg *self) NMP_OBJECT_TYPE_IP6_ADDRESS, NMP_OBJECT_TYPE_IP4_ROUTE, NMP_OBJECT_TYPE_IP6_ROUTE, + NMP_OBJECT_TYPE_IP4_NEXTHOP, + NMP_OBJECT_TYPE_IP6_NEXTHOP, }; ObjStateData *obj_state; int i; @@ -1321,6 +1336,23 @@ _commit_collect_addresses(NML3Cfg *self, int addr_family, NML3CfgCommitType comm (gpointer) &sync_filter_data); } +static GPtrArray * +_commit_collect_nexthops(NML3Cfg *self, int addr_family, NML3CfgCommitType commit_type) +{ + const int IS_IPv4 = NM_IS_IPv4(addr_family); + const NMDedupMultiHeadEntry *head_entry; + const ObjStatesSyncFilterData sync_filter_data = { + .self = self, + .commit_type = commit_type, + }; + + head_entry = nm_l3_config_data_lookup_objs(self->priv.p->combined_l3cd_commited, + NMP_OBJECT_TYPE_IP_NEXTHOP(IS_IPv4)); + return nm_dedup_multi_objs_to_ptr_array_head(head_entry, + _obj_states_sync_filter_predicate, + (gpointer) &sync_filter_data); +} + static void _commit_collect_routes(NML3Cfg *self, int addr_family, @@ -5486,13 +5518,16 @@ _l3_commit_one(NML3Cfg *self, NML3CfgCommitType commit_type, const NML3ConfigData *l3cd_old) { - const int IS_IPv4 = NM_IS_IPv4(addr_family); - gs_unref_ptrarray GPtrArray *addresses = NULL; - gs_unref_ptrarray GPtrArray *routes = NULL; - gs_unref_ptrarray GPtrArray *routes_nodev = NULL; - gs_unref_ptrarray GPtrArray *addresses_prune = NULL; - gs_unref_ptrarray GPtrArray *routes_prune = NULL; - gs_unref_ptrarray GPtrArray *routes_failed = NULL; + const int IS_IPv4 = NM_IS_IPv4(addr_family); + gs_unref_ptrarray GPtrArray *addresses = NULL; + gs_unref_ptrarray GPtrArray *nexthops = NULL; + gs_unref_ptrarray GPtrArray *routes = NULL; + gs_unref_ptrarray GPtrArray *routes_nodev = NULL; + gs_unref_ptrarray GPtrArray *addresses_prune = NULL; + gs_unref_ptrarray GPtrArray *nexthops_prune = NULL; + gs_unref_ptrarray GPtrArray *nexthops_platform = NULL; + gs_unref_ptrarray GPtrArray *routes_prune = NULL; + gs_unref_ptrarray GPtrArray *routes_failed = NULL; NMIPRouteTableSyncMode route_table_sync; char sbuf_commit_type[50]; guint i; @@ -5513,6 +5548,7 @@ _l3_commit_one(NML3Cfg *self, any_dirty = _obj_states_track_mark_dirty(self, TRUE); addresses = _commit_collect_addresses(self, addr_family, commit_type); + nexthops = _commit_collect_nexthops(self, addr_family, commit_type); _commit_collect_routes(self, addr_family, @@ -5594,17 +5630,46 @@ _l3_commit_one(NML3Cfg *self, route_table_sync, routes_old); + nexthops_platform = + nm_platform_ip_nexthop_dump(self->priv.platform, addr_family, self->priv.ifindex); + nexthops_prune = nm_g_ptr_array_ref(nexthops_platform); + _obj_state_zombie_lst_prune_all(self, addr_family); } } else { if (c_list_is_empty(&self->priv.p->blocked_lst_head_x[IS_IPv4])) { + GHashTable *ht = self->priv.p->nexthop_configured_ids_x[IS_IPv4]; + GHashTableIter h_iter; + gpointer key; + _obj_state_zombie_lst_get_prune_lists(self, addr_family, &addresses_prune, &routes_prune); + + /* Unlike addresses and routes, nexthop pruning doesn't use the + * zombie mechanism. On update, build the nexthop prune list from the internally + * tracked set of IDs that NM has configured. */ + g_hash_table_iter_init(&h_iter, ht); + while (g_hash_table_iter_next(&h_iter, &key, NULL)) { + NMPObject *obj; + + obj = nmp_object_new(NMP_OBJECT_TYPE_IP_NEXTHOP(IS_IPv4), NULL); + obj->ip_nexthop.id = GPOINTER_TO_UINT(key); + + if (!nexthops_prune) + nexthops_prune = + g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref); + g_ptr_array_add(nexthops_prune, obj); + } } } + if (!nexthops_platform) { + nexthops_platform = + nm_platform_ip_nexthop_dump(self->priv.platform, addr_family, self->priv.ifindex); + } + if (self->priv.ifindex == NM_LOOPBACK_IFINDEX) { if (!addresses) { NMPlatformIPXAddress ax; @@ -5639,6 +5704,21 @@ _l3_commit_one(NML3Cfg *self, ? NMP_IP_ADDRESS_SYNC_FLAGS_NONE : NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE); + nm_platform_ip_nexthop_sync(self->priv.platform, + addr_family, + self->priv.ifindex, + nexthops, + nexthops_prune, + nexthops_platform); + + /* Update the set of nexthop IDs we have configured. */ + g_hash_table_remove_all(self->priv.p->nexthop_configured_ids_x[IS_IPv4]); + for (i = 0; i < nm_g_ptr_array_len(nexthops); i++) { + guint32 id = NMP_OBJECT_CAST_IP_NEXTHOP(nexthops->pdata[i])->id; + + g_hash_table_add(self->priv.p->nexthop_configured_ids_x[IS_IPv4], GUINT_TO_POINTER(id)); + } + self->priv.p->commit_reentrant_count_ip_address_sync_x[IS_IPv4]--; _nodev_routes_sync(self, addr_family, commit_type, routes_nodev); @@ -6385,6 +6465,9 @@ nm_l3cfg_init(NML3Cfg *self) _obj_state_data_free, NULL); + self->priv.p->nexthop_configured_ids_4 = g_hash_table_new(nm_direct_hash, NULL); + self->priv.p->nexthop_configured_ids_6 = g_hash_table_new(nm_direct_hash, NULL); + nm_prioq_init(&self->priv.p->failedobj_prioq, _failedobj_prioq_cmp); } @@ -6464,6 +6547,9 @@ finalize(GObject *object) nm_assert(c_list_is_empty(&self->priv.p->obj_state_lst_head)); nm_assert(c_list_is_empty(&self->priv.p->obj_state_zombie_lst_head)); + nm_clear_pointer(&self->priv.p->nexthop_configured_ids_4, g_hash_table_destroy); + nm_clear_pointer(&self->priv.p->nexthop_configured_ids_6, g_hash_table_destroy); + if (_nodev_routes_untrack(self, AF_INET)) nmp_global_tracker_sync(self->priv.global_tracker, NMP_OBJECT_TYPE_IP4_ROUTE, FALSE); if (_nodev_routes_untrack(self, AF_INET6)) From ce2078dd4fe148002570e135150ef0bd56e3f33e Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:19:45 +0100 Subject: [PATCH 6/9] device: also flush nexthops --- src/core/devices/nm-device.c | 3 +++ src/core/devices/wwan/nm-modem.c | 1 + 2 files changed, 4 insertions(+) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 434683d6b4..1103a24185 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -13446,6 +13446,9 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family) * addresses and routes on activation. */ if (ip_ifindex > 0) { + nm_platform_ip_nexthop_flush(nm_device_get_platform(self), + AF_INET6, + ip_ifindex); nm_platform_ip_route_flush(nm_device_get_platform(self), AF_INET6, ip_ifindex); diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c index 9d8f61c55c..9018298169 100644 --- a/src/core/devices/wwan/nm-modem.c +++ b/src/core/devices/wwan/nm-modem.c @@ -1237,6 +1237,7 @@ deactivate_cleanup(NMModem *self, NMDevice *device, gboolean stop_ppp_manager) if (ifindex > 0) { NMPlatform *platform = nm_device_get_platform(device); + nm_platform_ip_nexthop_flush(platform, AF_UNSPEC, ifindex); nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex); nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex); nm_platform_link_change_flags(platform, ifindex, IFF_UP, FALSE); From 13f83149e1a96d115d3959197eff4c6106adcb6c Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Mon, 9 Feb 2026 13:37:52 +0100 Subject: [PATCH 7/9] core: add nexthop id reservation function in the netns When an IP configuration method (e.g. NDisc) wants to use a nexthop, it needs to allocate a free id. Add functions to keep track of the used ids in the namespace so that we avoid collisions. --- src/core/nm-netns.c | 79 +++++++++++++++++++++++++++++++++++++++++++++ src/core/nm-netns.h | 7 ++++ 2 files changed, 86 insertions(+) diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c index faae72670d..1cf9d4d5fb 100644 --- a/src/core/nm-netns.c +++ b/src/core/nm-netns.c @@ -83,6 +83,11 @@ typedef struct { * by IP address. */ GHashTable *watcher_ip_data_idx; + /* Tracks reserved nexthop IDs. Maps guint32 id -> gconstpointer tag. + * This is used to avoid nexthop ID collisions across different + * interfaces/ndisc instances within the same network namespace. */ + GHashTable *nexthop_id_reserved; + CList l3cfg_signal_pending_lst_head; GSource *signal_pending_idle_source; } NMNetnsPrivate; @@ -1521,6 +1526,76 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps /*****************************************************************************/ +gboolean +nm_netns_nexthop_id_is_reserved(NMNetns *self, guint32 id) +{ + NMNetnsPrivate *priv; + + g_return_val_if_fail(NM_IS_NETNS(self), FALSE); + nm_assert(id != 0); + + priv = NM_NETNS_GET_PRIVATE(self); + return g_hash_table_contains(priv->nexthop_id_reserved, GUINT_TO_POINTER(id)); +} + +gboolean +nm_netns_nexthop_id_reserve(NMNetns *self, guint32 id, gconstpointer tag) +{ + NMNetnsPrivate *priv; + gpointer existing_tag; + + g_return_val_if_fail(NM_IS_NETNS(self), FALSE); + nm_assert(id != 0); + nm_assert(tag); + + priv = NM_NETNS_GET_PRIVATE(self); + + if (g_hash_table_lookup_extended(priv->nexthop_id_reserved, + GUINT_TO_POINTER(id), + NULL, + &existing_tag)) { + /* The ID is already reserved. If it's by the same tag, it's + * idempotent; otherwise it's a collision. */ + return existing_tag == tag; + } + + g_hash_table_insert(priv->nexthop_id_reserved, GUINT_TO_POINTER(id), (gpointer) tag); + return TRUE; +} + +void +nm_netns_nexthop_id_release(NMNetns *self, guint32 id) +{ + NMNetnsPrivate *priv; + + g_return_if_fail(NM_IS_NETNS(self)); + nm_assert(id != 0); + + priv = NM_NETNS_GET_PRIVATE(self); + g_hash_table_remove(priv->nexthop_id_reserved, GUINT_TO_POINTER(id)); +} + +void +nm_netns_nexthop_id_release_all(NMNetns *self, gconstpointer tag) +{ + NMNetnsPrivate *priv; + GHashTableIter iter; + gpointer value; + + g_return_if_fail(NM_IS_NETNS(self)); + nm_assert(tag); + + priv = NM_NETNS_GET_PRIVATE(self); + + g_hash_table_iter_init(&iter, priv->nexthop_id_reserved); + while (g_hash_table_iter_next(&iter, NULL, &value)) { + if (value == tag) + g_hash_table_iter_remove(&iter); + } +} + +/*****************************************************************************/ + static void nm_netns_init(NMNetns *self) { @@ -1545,6 +1620,8 @@ nm_netns_init(NMNetns *self) (GDestroyNotify) _watcher_by_tag_destroy, NULL); priv->watcher_ip_data_idx = g_hash_table_new(_watcher_ip_data_hash, _watcher_ip_data_equal); + + priv->nexthop_id_reserved = g_hash_table_new(nm_direct_hash, NULL); } static void @@ -1637,6 +1714,8 @@ dispose(GObject *object) nm_clear_pointer(&priv->watcher_by_tag_idx, g_hash_table_destroy); nm_clear_pointer(&priv->watcher_ip_data_idx, g_hash_table_destroy); + nm_clear_pointer(&priv->nexthop_id_reserved, g_hash_table_destroy); + nm_clear_g_source_inst(&priv->signal_pending_idle_source); if (priv->platform) diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h index e32d5680aa..c2414c7dfd 100644 --- a/src/core/nm-netns.h +++ b/src/core/nm-netns.h @@ -109,4 +109,11 @@ void nm_netns_watcher_add(NMNetns *self, void nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag); void nm_netns_watcher_remove_dirty(NMNetns *self, gconstpointer tag); +/*****************************************************************************/ + +gboolean nm_netns_nexthop_id_is_reserved(NMNetns *self, guint32 id); +gboolean nm_netns_nexthop_id_reserve(NMNetns *self, guint32 id, gconstpointer tag); +void nm_netns_nexthop_id_release(NMNetns *self, guint32 id); +void nm_netns_nexthop_id_release_all(NMNetns *self, gconstpointer tag); + #endif /* __NM_NETNS_H__ */ From 359762b2c9f9d23a07370bfbd3f57e3d90f126f2 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 10 Dec 2025 14:19:57 +0100 Subject: [PATCH 8/9] ndisc: generate IPv6 routes with nexthops to avoid merging Previously the NDisc code would generate multiple IPv6 default routes with the same metric. The kernel then would merge them into a single ECMP route, which is forbidden by RFCs as it breaks NUD and other use cases. When the kernel is managing IPv6 on the interface it is able to add multiple independent default routes because it sets the internal flag RTF_ADDRCONF on them, which prevents ECMP. This flag can't be set from userspace, and proposals to expose it were rejected: https://lore.kernel.org/netdev/20241105031841.10730-2-Matt.Muggeridge@hpe.com/ The only way to avoid the merge is to use the nexthop API. Now when there are multiple default routes, the NDisc uses a nexthop for them. The nexthop id is generated by hashing the route destination (::/0 for default routes), the interface name and the gateway, so that the id remains stable across restarts. In case of collisions with other nexthops created by NetworkManager, or with nexthops configured externally, a different id is chosen. To mitigate the chance of collisions with external nexthops, we only choose ids with the high bit set. --- src/core/ndisc/nm-ndisc.c | 174 +++++++++++++++++++++++++++++++++----- src/core/ndisc/nm-ndisc.h | 1 + 2 files changed, 153 insertions(+), 22 deletions(-) diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c index ed34f088a0..306e1660b2 100644 --- a/src/core/ndisc/nm-ndisc.c +++ b/src/core/ndisc/nm-ndisc.c @@ -18,6 +18,7 @@ #include "nm-l3-config-data.h" #include "nm-l3cfg.h" #include "nm-ndisc-private.h" +#include "nm-netns.h" #include "nm-setting-ip6-config.h" #include "nm-utils.h" @@ -105,6 +106,77 @@ NM_UTILS_LOOKUP_STR_DEFINE(nm_ndisc_dhcp_level_to_string, /*****************************************************************************/ +#define NEXTHOP_ID_RETRIES 400 + +static guint32 +nexthop_id_alloc(NMNDisc *ndisc, + const struct in6_addr *dest, + guint plen, + const struct in6_addr *gateway) +{ + NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE(ndisc); + NMNetns *netns = nm_l3cfg_get_netns(priv->config.l3cfg); + const char *ifname = nm_l3cfg_get_ifname(priv->config.l3cfg, FALSE); + NMPlatform *platform = nm_l3cfg_get_platform(priv->config.l3cfg); + int ifindex = nm_l3cfg_get_ifindex(priv->config.l3cfg); + nm_auto_nmpobj NMPObject *obj = NULL; + CSipHash state; + guint64 id64; + guint32 id; + guint i; + + /* Determine a stable nexthop ID by hashing the interface name, the destination + * and the gateway. We set the high bit to decrease the chance of collisions with + * external (manually added) nexthops. */ + c_siphash_init(&state, NM_HASH_SEED_16_U64(725697701u)); + c_siphash_append(&state, (const uint8_t *) ifname, strlen(ifname) + 1); + c_siphash_append(&state, (const uint8_t *) dest, sizeof(struct in6_addr)); + c_siphash_append(&state, (const uint8_t *) &plen, sizeof(guint)); + c_siphash_append(&state, (const uint8_t *) gateway, sizeof(struct in6_addr)); + id64 = c_siphash_finalize(&state); + id = ((guint32) (id64 >> 32u)) | (1u << 31); + + for (i = 0; i < NEXTHOP_ID_RETRIES; i++, id++) { + if (i < NEXTHOP_ID_RETRIES * 3 / 4) { + id |= (1u << 31); + } else { + /* After many collisions, start probing random ids */ + id = (guint32) nm_random_u64_range(1u << 31, G_MAXUINT32); + } + + if (nm_netns_nexthop_id_is_reserved(netns, id)) + continue; + + if (nm_platform_ip_nexthop_get(platform, id, &obj)) { + /* The id already exists in platform. We can reuse it only + * if it's an IPv6 RA nexthop on the same interface. */ + if (NMP_OBJECT_GET_TYPE(obj) != NMP_OBJECT_TYPE_IP6_NEXTHOP + || obj->ip6_nexthop.nh_source != NM_IP_CONFIG_SOURCE_RTPROT_RA + || obj->ip6_nexthop.ifindex != ifindex) + continue; + } + + nm_netns_nexthop_id_reserve(netns, id, ndisc); + return id; + } + + return 0; +} + +static void +_nexthop_id_release_one(NMNDisc *ndisc, guint32 nexthop_id) +{ + NMNDiscPrivate *priv; + + if (nexthop_id == 0) + return; + + priv = NM_NDISC_GET_PRIVATE(ndisc); + nm_netns_nexthop_id_release(nm_l3cfg_get_netns(priv->config.l3cfg), nexthop_id); +} + +/*****************************************************************************/ + NML3ConfigData * nm_ndisc_data_to_l3cd(NMDedupMultiIndex *multi_idx, int ifindex, @@ -172,9 +244,6 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex *multi_idx, } if (rdata->gateways_n > 0) { - guint metric_offset = 0; - NMIcmpv6RouterPref prev_pref = NM_ICMPV6_ROUTER_PREF_INVALID; - NMPlatformIP6Route r = { .rt_source = NM_IP_CONFIG_SOURCE_NDISC, .ifindex = ifindex, @@ -185,24 +254,38 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex *multi_idx, }; for (i = 0; i < rdata->gateways_n; i++) { - /* If we add multiple default routes with the same metric and - * different preferences, kernel merges them into a single ECMP - * route, with overall preference equal to the preference of the - * first route added. Therefore, the preference of individual routes - * is not respected. - * To avoid that, add routes with different metrics if they have - * different preferences, so that they are not merged together. Here - * the gateways are already ordered by increasing preference. */ - if (i != 0 && rdata->gateways[i].preference != prev_pref) { - metric_offset++; - } + NMPlatformIP6NextHop nh; - prev_pref = rdata->gateways[i].preference; - r.metric = metric_offset; - r.gateway = rdata->gateways[i].address; r.rt_pref = rdata->gateways[i].preference; nm_assert((NMIcmpv6RouterPref) r.rt_pref == rdata->gateways[i].preference); + + /* If we add multiple routes with the same destination (in this case, the + * default route) and the same metric, the kernel merges them into a single + * ECMP route, which is forbidden by RFCs as it breaks NUD and other use cases. + * Use nexthop objects to avoid this merging behavior. + * + * We could use nexthops only when there are multiple default routes on this + * interface. But that is not enough, because there can be multiple profiles + * with the same ipv6.route-metric value, and their default routes would still + * be merged. We need to always use nexthops. + */ + + if (rdata->gateways[i].nexthop_id == 0) { + /* The nexthop id could not be reserved and we already emitted a warning */ + continue; + } + + nh = (NMPlatformIP6NextHop) { + .ifindex = ifindex, + .nh_source = NM_IP_CONFIG_SOURCE_NDISC, + .gateway = rdata->gateways[i].address, + .id = rdata->gateways[i].nexthop_id, + }; + + r.nhid = nh.id; + r.gateway = nh.gateway; nm_l3_config_data_add_route_6(l3cd, &r); + nm_l3_config_data_add_nexthop(l3cd, AF_INET6, NULL, (const NMPlatformIPNextHop *) &nh); } } @@ -465,23 +548,38 @@ nm_ndisc_add_gateway(NMNDisc *ndisc, const NMNDiscGateway *new_item, gint64 now_ { NMNDiscDataInternal *rdata = &NM_NDISC_GET_PRIVATE(ndisc)->rdata; guint i; - guint insert_idx = G_MAXUINT; + guint insert_idx = G_MAXUINT; + guint32 old_nexthop_id = 0; + NMNDiscGateway gw; for (i = 0; i < rdata->gateways->len;) { NMNDiscGateway *item = &nm_g_array_index(rdata->gateways, NMNDiscGateway, i); if (IN6_ARE_ADDR_EQUAL(&item->address, &new_item->address)) { if (new_item->expiry_msec <= now_msec) { + _nexthop_id_release_one(ndisc, item->nexthop_id); g_array_remove_index(rdata->gateways, i); _ASSERT_data_gateways(rdata); return TRUE; } if (item->preference != new_item->preference) { + /* Preference changed: save the nexthop ID so that we can + * reuse it when re-inserting at the correct position. */ + old_nexthop_id = item->nexthop_id; g_array_remove_index(rdata->gateways, i); continue; } + if (item->nexthop_id == 0) { + /* We failed to allocate the nexthop id previously; retry. */ + item->nexthop_id = nexthop_id_alloc(ndisc, &in6addr_any, 0, &new_item->address); + if (item->nexthop_id > 0) { + item->expiry_msec = new_item->expiry_msec; + return TRUE; + } + } + if (item->expiry_msec == new_item->expiry_msec) return FALSE; @@ -505,9 +603,26 @@ nm_ndisc_add_gateway(NMNDisc *ndisc, const NMNDiscGateway *new_item, gint64 now_ if (new_item->expiry_msec <= now_msec) return FALSE; + /* Make a copy of the gateway and assign a nexthop id, reusing the existing + * one if possible */ + gw = *new_item; + if (old_nexthop_id != 0) { + gw.nexthop_id = old_nexthop_id; + } else { + gw.nexthop_id = nexthop_id_alloc(ndisc, &in6addr_any, 0, &new_item->address); + if (gw.nexthop_id == 0) { + char buf[INET6_ADDRSTRLEN]; + + _LOGW("failed to find a free nexthop id for gateway %s", + nm_inet6_ntop(&new_item->address, buf)); + return FALSE; + } + } + g_array_insert_val(rdata->gateways, insert_idx == G_MAXUINT ? rdata->gateways->len : insert_idx, - *new_item); + gw); + _ASSERT_data_gateways(rdata); return TRUE; } @@ -1330,6 +1445,9 @@ nm_ndisc_stop(NMNDisc *ndisc) NM_NDISC_GET_CLASS(ndisc)->stop(ndisc); + /* Release all nexthop IDs reserved by this ndisc instance. */ + nm_netns_nexthop_id_release_all(nm_l3cfg_get_netns(priv->config.l3cfg), ndisc); + rdata = &priv->rdata; g_array_set_size(rdata->gateways, 0); @@ -1442,9 +1560,10 @@ _config_changed_log(NMNDisc *ndisc, NMNDiscConfigMap changed) for (i = 0; i < rdata->gateways->len; i++) { const NMNDiscGateway *gateway = &nm_g_array_index(rdata->gateways, NMNDiscGateway, i); - _LOGD(" gateway %s pref %s exp %s", + _LOGD(" gateway %s pref %s nhid %u exp %s", nm_inet6_ntop(&gateway->address, addrstr), nm_icmpv6_router_pref_to_string(gateway->preference, str_pref, sizeof(str_pref)), + gateway->nexthop_id, get_exp(str_exp, now_msec, gateway)); } for (i = 0; i < rdata->addresses->len; i++) { @@ -1521,8 +1640,11 @@ clean_gateways(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint6 arr = &nm_g_array_first(rdata->gateways, NMNDiscGateway); for (i = 0, j = 0; i < rdata->gateways->len; i++) { - if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec)) + if (!expiry_next(now_msec, arr[i].expiry_msec, next_msec)) { + /* Gateway expired. Release its nexthop ID. */ + _nexthop_id_release_one(ndisc, arr[i].nexthop_id); continue; + } if (i != j) arr[j] = arr[i]; j++; @@ -1533,8 +1655,14 @@ clean_gateways(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint6 g_array_set_size(rdata->gateways, j); } - if (_array_set_size_max(rdata->gateways, _SIZE_MAX_GATEWAYS)) + if (rdata->gateways->len > _SIZE_MAX_GATEWAYS) { + for (i = _SIZE_MAX_GATEWAYS; i < rdata->gateways->len; i++) + _nexthop_id_release_one( + ndisc, + nm_g_array_index(rdata->gateways, NMNDiscGateway, i).nexthop_id); + g_array_set_size(rdata->gateways, _SIZE_MAX_GATEWAYS); *changed |= NM_NDISC_CONFIG_GATEWAYS; + } _ASSERT_data_gateways(rdata); } @@ -2065,6 +2193,8 @@ finalize(GObject *object) NMNDiscPrivate *priv = NM_NDISC_GET_PRIVATE(ndisc); NMNDiscDataInternal *rdata = &priv->rdata; + nm_netns_nexthop_id_release_all(nm_l3cfg_get_netns(priv->config.l3cfg), ndisc); + g_array_unref(rdata->gateways); g_array_unref(rdata->addresses); g_array_unref(rdata->routes); diff --git a/src/core/ndisc/nm-ndisc.h b/src/core/ndisc/nm-ndisc.h index 5b2efde569..6d75417b6c 100644 --- a/src/core/ndisc/nm-ndisc.h +++ b/src/core/ndisc/nm-ndisc.h @@ -100,6 +100,7 @@ typedef struct _NMNDiscGateway { struct in6_addr address; gint64 expiry_msec; NMIcmpv6RouterPref preference; + guint32 nexthop_id; } NMNDiscGateway; typedef struct _NMNDiscAddress { From 3d910a9c3dfdb8b6920503f56b16ad672f249a62 Mon Sep 17 00:00:00 2001 From: Beniamino Galvani Date: Wed, 11 Feb 2026 20:00:37 +0100 Subject: [PATCH 9/9] ndisc: fix truncation of excess RA data Fixes: c2c8c67d8c45 ('ndisc: rate limit number of accepted RA data to track') --- src/core/ndisc/nm-ndisc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c index 306e1660b2..2016408801 100644 --- a/src/core/ndisc/nm-ndisc.c +++ b/src/core/ndisc/nm-ndisc.c @@ -1724,7 +1724,7 @@ clean_routes(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gint64 g_array_set_size(rdata->routes, j); } - if (_array_set_size_max(rdata->gateways, _SIZE_MAX_ROUTES)) + if (_array_set_size_max(rdata->routes, _SIZE_MAX_ROUTES)) *changed |= NM_NDISC_CONFIG_ROUTES; } @@ -1793,7 +1793,7 @@ clean_dns_servers(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi g_array_set_size(rdata->dns_servers, j); } - if (_array_set_size_max(rdata->gateways, _SIZE_MAX_DNS_SERVERS)) + if (_array_set_size_max(rdata->dns_servers, _SIZE_MAX_DNS_SERVERS)) *changed |= NM_NDISC_CONFIG_DNS_SERVERS; } @@ -1828,7 +1828,7 @@ clean_dns_domains(NMNDisc *ndisc, gint64 now_msec, NMNDiscConfigMap *changed, gi g_array_set_size(rdata->dns_domains, j); } - if (_array_set_size_max(rdata->gateways, _SIZE_MAX_DNS_DOMAINS)) + if (_array_set_size_max(rdata->dns_domains, _SIZE_MAX_DNS_DOMAINS)) *changed |= NM_NDISC_CONFIG_DNS_DOMAINS; }