summary refs log tree commit diff
path: root/src/libnm-platform
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2026-05-14 19:21:22 +0200
committerMichael Biebl <biebl@debian.org>2026-05-14 19:21:22 +0200
commit869e9027026cdbb15d4e4a6327ff41d2697858eb (patch)
tree52edec7f935ff1bbb425c3515c6e49e6b8b0e28b /src/libnm-platform
parent067fb576988f685e83ac8b0ae690334aff547c85 (diff)
New upstream version 1.56.1 upstream/1.56.1
Diffstat (limited to 'src/libnm-platform')
-rw-r--r--src/libnm-platform/nm-linux-platform.c131
-rw-r--r--src/libnm-platform/nm-platform.c109
-rw-r--r--src/libnm-platform/nm-platform.h45
-rw-r--r--src/libnm-platform/nmp-base.h7
-rw-r--r--src/libnm-platform/nmp-object.c12
-rw-r--r--src/libnm-platform/nmp-object.h8
-rw-r--r--src/libnm-platform/tests/test-nm-platform.c2
7 files changed, 279 insertions, 35 deletions
diff --git a/src/libnm-platform/nm-linux-platform.c b/src/libnm-platform/nm-linux-platform.c
index cc5b99e0..b5df8b17 100644
--- a/src/libnm-platform/nm-linux-platform.c
+++ b/src/libnm-platform/nm-linux-platform.c
@@ -862,6 +862,7 @@ static const LinkDesc link_descs[] = {
 
     [NM_LINK_TYPE_BNEP]        = {"bluetooth", NULL, "bluetooth"},
     [NM_LINK_TYPE_DUMMY]       = {"dummy", "dummy", NULL},
+    [NM_LINK_TYPE_GENEVE]      = {"geneve", "geneve", "geneve"},
     [NM_LINK_TYPE_GRE]         = {"gre", "gre", NULL},
     [NM_LINK_TYPE_GRETAP]      = {"gretap", "gretap", NULL},
     [NM_LINK_TYPE_IFB]         = {"ifb", "ifb", NULL},
@@ -909,6 +910,7 @@ _link_type_from_rtnl_type(const char *name)
         NM_LINK_TYPE_BOND,        /* "bond"        */
         NM_LINK_TYPE_BRIDGE,      /* "bridge"      */
         NM_LINK_TYPE_DUMMY,       /* "dummy"       */
+        NM_LINK_TYPE_GENEVE,      /* "geneve"      */
         NM_LINK_TYPE_GRE,         /* "gre"         */
         NM_LINK_TYPE_GRETAP,      /* "gretap"      */
         NM_LINK_TYPE_HSR,         /* "hsr"         */
@@ -987,6 +989,7 @@ _link_type_from_devtype(const char *name)
         NM_LINK_TYPE_BNEP,      /* "bluetooth" */
         NM_LINK_TYPE_BOND,      /* "bond"      */
         NM_LINK_TYPE_BRIDGE,    /* "bridge"    */
+        NM_LINK_TYPE_GENEVE,    /* "geneve"    */
         NM_LINK_TYPE_HSR,       /* "hsr"       */
         NM_LINK_TYPE_PPP,       /* "ppp"       */
         NM_LINK_TYPE_VLAN,      /* "vlan"      */
@@ -1857,6 +1860,57 @@ _parse_lnk_gre(const char *kind, struct nlattr *info_data)
 /*****************************************************************************/
 
 static NMPObject *
+_parse_lnk_geneve(const char *kind, struct nlattr *info_data)
+{
+    static const struct nla_policy policy[] = {
+        [IFLA_GENEVE_ID]          = {.type = NLA_U32},
+        [IFLA_GENEVE_REMOTE]      = {.type = NLA_U32},
+        [IFLA_GENEVE_REMOTE6]     = {.type = NLA_UNSPEC, .minlen = sizeof(struct in6_addr)},
+        [IFLA_GENEVE_TTL]         = {.type = NLA_U8},
+        [IFLA_GENEVE_TOS]         = {.type = NLA_U8},
+        [IFLA_GENEVE_TTL_INHERIT] = {.type = NLA_U8},
+        [IFLA_GENEVE_PORT]        = {.type = NLA_U16},
+        [IFLA_GENEVE_DF]          = {.type = NLA_U8},
+    };
+
+    struct nlattr       *tb[G_N_ELEMENTS(policy)];
+    NMPObject           *obj;
+    NMPlatformLnkGeneve *props;
+
+    if (!info_data || !nm_streq0(kind, "geneve"))
+        return NULL;
+
+    if (nla_parse_nested_arr(tb, info_data, policy) < 0)
+        return NULL;
+
+    obj   = nmp_object_new(NMP_OBJECT_TYPE_LNK_GENEVE, NULL);
+    props = &obj->lnk_geneve;
+
+    if (tb[IFLA_GENEVE_ID])
+        props->id = nla_get_u32(tb[IFLA_GENEVE_ID]);
+    if (tb[IFLA_GENEVE_REMOTE])
+        props->remote = nla_get_u32(tb[IFLA_GENEVE_REMOTE]);
+    if (tb[IFLA_GENEVE_REMOTE6])
+        props->remote6 = *nla_data_as(struct in6_addr, tb[IFLA_GENEVE_REMOTE6]);
+
+    if (tb[IFLA_GENEVE_TTL_INHERIT] && nla_get_u8(tb[IFLA_GENEVE_TTL_INHERIT]))
+        props->ttl = -1;
+    else if (tb[IFLA_GENEVE_TTL])
+        props->ttl = nla_get_u8(tb[IFLA_GENEVE_TTL]);
+
+    if (tb[IFLA_GENEVE_TOS])
+        props->tos = nla_get_u8(tb[IFLA_GENEVE_TOS]);
+    if (tb[IFLA_GENEVE_PORT])
+        props->dst_port = ntohs(nla_get_u16(tb[IFLA_GENEVE_PORT]));
+    if (tb[IFLA_GENEVE_DF])
+        props->df = nla_get_u8(tb[IFLA_GENEVE_DF]);
+
+    return obj;
+}
+
+/*****************************************************************************/
+
+static NMPObject *
 _parse_lnk_hsr(const char *kind, struct nlattr *info_data)
 {
     static const struct nla_policy policy[] = {
@@ -3694,6 +3748,9 @@ _new_from_nl_link(NMPlatform            *platform,
     case NM_LINK_TYPE_BOND:
         lnk_data = _parse_lnk_bond(nl_info_kind, nl_info_data);
         break;
+    case NM_LINK_TYPE_GENEVE:
+        lnk_data = _parse_lnk_geneve(nl_info_kind, nl_info_data);
+        break;
     case NM_LINK_TYPE_GRE:
     case NM_LINK_TYPE_GRETAP:
         lnk_data = _parse_lnk_gre(nl_info_kind, nl_info_data);
@@ -4033,6 +4090,7 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
         int      ifindex;
         NMIPAddr gateway;
         gboolean is_via;
+        unsigned rtnh_flags;
     } nh = {
         .found    = FALSE,
         .has_more = FALSE,
@@ -4142,9 +4200,10 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
                     v4_nh_extra_nexthops = v4_nh_extra_nexthops_heap;
                 }
                 nm_assert(v4_n_nexthops - 1u < v4_nh_extra_alloc);
-                new_nexthop          = &v4_nh_extra_nexthops[v4_n_nexthops - 1u];
-                new_nexthop->ifindex = rtnh->rtnh_ifindex;
-                new_nexthop->weight  = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
+                new_nexthop             = &v4_nh_extra_nexthops[v4_n_nexthops - 1u];
+                new_nexthop->ifindex    = rtnh->rtnh_ifindex;
+                new_nexthop->weight     = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
+                new_nexthop->rtnh_flags = rtnh->rtnh_flags;
                 if (rtnh->rtnh_len > sizeof(*rtnh)) {
                     struct nlattr *ntb[RTA_MAX + 1];
 
@@ -4159,9 +4218,10 @@ _new_from_nl_route(const struct nlmsghdr *nlh, gboolean id_only, ParseNlmsgIter
                         memcpy(&new_nexthop->gateway, nla_data(ntb[RTA_GATEWAY]), addr_len);
                 }
             } else if (IS_IPv4 || idx == multihop_idx) {
-                nh.found   = TRUE;
-                nh.ifindex = rtnh->rtnh_ifindex;
-                nh.weight  = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
+                nh.found      = TRUE;
+                nh.ifindex    = rtnh->rtnh_ifindex;
+                nh.weight     = NM_MAX(((guint) rtnh->rtnh_hops) + 1u, 1u);
+                nh.rtnh_flags = rtnh->rtnh_flags;
                 if (rtnh->rtnh_len > sizeof(*rtnh)) {
                     struct nlattr *ntb[RTA_MAX + 1];
 
@@ -4409,7 +4469,16 @@ rta_multipath_done:
     }
 
     obj->ip_route.r_rtm_flags = rtm->rtm_flags;
-    obj->ip_route.rt_source   = nmp_utils_ip_config_source_from_rtprot(rtm->rtm_protocol);
+
+    if (IS_IPv4 && v4_n_nexthops > 1u) {
+        /* For multipath routes, rtm_flags at the route level does not contain
+         * RTNH_F_ONLINK. Instead, each nexthop has its own rtnh_flags. Merge the
+         * first nexthop's RTNH_F_ONLINK into r_rtm_flags, since the first nexthop
+         * is embedded in the route struct. */
+        obj->ip_route.r_rtm_flags |= (nh.rtnh_flags & RTNH_F_ONLINK);
+    }
+
+    obj->ip_route.rt_source = nmp_utils_ip_config_source_from_rtprot(rtm->rtm_protocol);
 
     if (nh.has_more) {
         parse_nlmsg_iter->iter_more               = TRUE;
@@ -5182,6 +5251,35 @@ _nl_msg_new_link_set_linkinfo(struct nl_msg *msg, NMLinkType link_type, gconstpo
         nla_nest_end(msg, info_peer);
         break;
     }
+    case NM_LINK_TYPE_GENEVE:
+    {
+        const NMPlatformLnkGeneve *props = extra_data;
+
+        nm_assert(props);
+
+        if (!(data = nla_nest_start(msg, IFLA_INFO_DATA)))
+            goto nla_put_failure;
+
+        NLA_PUT_U32(msg, IFLA_GENEVE_ID, props->id);
+
+        if (props->remote) {
+            NLA_PUT_U32(msg, IFLA_GENEVE_REMOTE, props->remote);
+        } else if (!IN6_IS_ADDR_UNSPECIFIED(&props->remote6)) {
+            NLA_PUT(msg, IFLA_GENEVE_REMOTE6, sizeof(props->remote6), &props->remote6);
+        }
+        NLA_PUT_U16(msg, IFLA_GENEVE_PORT, htons(props->dst_port));
+        if (props->ttl == -1) {
+            NLA_PUT_U8(msg, IFLA_GENEVE_TTL_INHERIT, 1);
+        } else {
+            /* When you want to specify a TTL value,
+             * don't add TTL_INHERIT to the message */
+            NLA_PUT_U8(msg, IFLA_GENEVE_TTL, props->ttl & 0xff);
+        }
+        NLA_PUT_U8(msg, IFLA_GENEVE_TOS, props->tos);
+        NLA_PUT_U8(msg, IFLA_GENEVE_DF, props->df);
+        break;
+    }
+
     case NM_LINK_TYPE_GRE:
     case NM_LINK_TYPE_GRETAP:
     {
@@ -5813,15 +5911,18 @@ _nl_msg_new_route(uint16_t nlmsg_type, uint16_t nlmsg_flags, const NMPObject *ob
             }
             NLA_PUT_U32(msg, RTA_GATEWAY, gw);
 
-            rtnh->rtnh_flags = 0;
+            if (i == 0u) {
+                rtnh->rtnh_flags =
+                    (gw != 0 && NM_FLAGS_HAS(obj->ip_route.r_rtm_flags, (unsigned) RTNH_F_ONLINK))
+                        ? RTNH_F_ONLINK
+                        : 0;
+            } else {
+                const NMPlatformIP4RtNextHop *n2 = &obj->_ip4_route.extra_nexthops[i - 1u];
 
-            if (obj->ip4_route.n_nexthops > 1
-                && NM_FLAGS_HAS(obj->ip_route.r_rtm_flags, (unsigned) (RTNH_F_ONLINK)) && gw != 0) {
-                /* Unlike kernel, we only track the onlink flag per NMPlatformIP4Address, and
-                 * not per nexthop. That is fine for NetworkManager configuring addresses.
-                 * It is not fine for tracking addresses from kernel in platform cache,
-                 * because the rtnh_flags of the nexthops need to be part of nmp_object_id_cmp(). */
-                rtnh->rtnh_flags |= RTNH_F_ONLINK;
+                rtnh->rtnh_flags =
+                    (gw != 0 && NM_FLAGS_HAS(n2->rtnh_flags, (unsigned) RTNH_F_ONLINK))
+                        ? RTNH_F_ONLINK
+                        : 0;
             }
 
             rtnh->rtnh_len = (char *) nlmsg_tail(nlmsg_hdr(msg)) - (char *) rtnh;
diff --git a/src/libnm-platform/nm-platform.c b/src/libnm-platform/nm-platform.c
index 853157d2..914dcd86 100644
--- a/src/libnm-platform/nm-platform.c
+++ b/src/libnm-platform/nm-platform.c
@@ -1388,6 +1388,12 @@ nm_platform_link_add(NMPlatform            *self,
                case NM_LINK_TYPE_VETH:
                    nm_sprintf_buf(buf, ", veth-peer \"%s\"", (const char *) extra_data);
                    break;
+               case NM_LINK_TYPE_GENEVE:
+                   nm_strbuf_append_str(&buf_p, &buf_len, ", ");
+                   nm_platform_lnk_geneve_to_string((const NMPlatformLnkGeneve *) extra_data,
+                                                    buf_p,
+                                                    buf_len);
+                   break;
                case NM_LINK_TYPE_GRE:
                case NM_LINK_TYPE_GRETAP:
                    nm_strbuf_append_str(&buf_p, &buf_len, ", ");
@@ -2565,6 +2571,12 @@ nm_platform_link_get_lnk_bridge(NMPlatform *self, int ifindex, const NMPlatformL
     return _link_get_lnk(self, ifindex, NM_LINK_TYPE_BRIDGE, out_link);
 }
 
+const NMPlatformLnkGeneve *
+nm_platform_link_get_lnk_geneve(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
+{
+    return _link_get_lnk(self, ifindex, NM_LINK_TYPE_GENEVE, out_link);
+}
+
 const NMPlatformLnkGre *
 nm_platform_link_get_lnk_gre(NMPlatform *self, int ifindex, const NMPlatformLink **out_link)
 {
@@ -6495,6 +6507,52 @@ nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize le
 }
 
 const char *
+nm_platform_lnk_geneve_to_string(const NMPlatformLnkGeneve *lnk, char *buf, gsize len)
+{
+    char str_remote[NM_INET_ADDRSTRLEN];
+    char str_remote1[30 + NM_INET_ADDRSTRLEN];
+    char str_remote6[NM_INET_ADDRSTRLEN];
+    char str_remote6_1[30 + NM_INET_ADDRSTRLEN];
+    char str_ttl[30];
+    char str_tos[30];
+    char str_id[30];
+    char str_dstport[30];
+
+    if (!nm_utils_to_string_buffer_init_null(lnk, &buf, &len))
+        return buf;
+
+    g_snprintf(
+        buf,
+        len,
+        "geneve"
+        "%s" /* id */
+        "%s" /* remote */
+        "%s" /* remote6 */
+        "%s" /* dst_port */
+        "%s" /* ttl */
+        "%s" /* tos */
+        "%s" /* df */
+        "",
+        lnk->id ? nm_sprintf_buf(str_id, " id %u", lnk->id) : "",
+        lnk->remote
+            ? nm_sprintf_buf(str_remote, " remote %s", nm_inet4_ntop(lnk->remote, str_remote1))
+            : "",
+        !IN6_IS_ADDR_UNSPECIFIED(&lnk->remote6)
+            ? nm_sprintf_buf(str_remote6, " remote %s", nm_inet6_ntop(&lnk->remote6, str_remote6_1))
+            : "",
+        lnk->dst_port ? nm_sprintf_buf(str_dstport, " dstport %u", lnk->dst_port) : "",
+        lnk->ttl > 0    ? nm_sprintf_buf(str_ttl, " ttl %u", lnk->ttl & 0xff)
+        : lnk->ttl == 0 ? "ttl auto"
+                        : "ttl inherit",
+        lnk->tos ? (lnk->tos == 1 ? " tos inherit" : nm_sprintf_buf(str_tos, " tos 0x%x", lnk->tos))
+                 : "",
+        lnk->df == 1   ? " df set "
+        : lnk->df == 2 ? " df inherit "
+                       : "");
+    return buf;
+}
+
+const char *
 nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len)
 {
     char str_local[30];
@@ -7366,13 +7424,16 @@ nm_platform_ip4_route_to_string_full(const NMPlatformIP4Route     *route,
                         "%s"         /* ifindex */
                         "%s%s"       /* gateway */
                         " weight %s" /* weight */
+                        "%s"         /* onlink */
                         "",
                         NM_PRINT_FMT_QUOTED2(nexthop->gateway != 0 || nexthop->ifindex <= 0,
                                              " via ",
                                              nm_inet4_ntop(nexthop->gateway, s_gateway),
                                              ""),
                         _to_string_dev(str_dev, nexthop->ifindex),
-                        nm_sprintf_buf(weight_str, "%u", nexthop->weight));
+                        nm_sprintf_buf(weight_str, "%u", nexthop->weight),
+                        NM_FLAGS_HAS(nexthop->rtnh_flags, (unsigned) RTNH_F_ONLINK) ? " onlink"
+                                                                                    : "");
                 }
             }
         }
@@ -8492,6 +8553,27 @@ nm_platform_lnk_gre_cmp(const NMPlatformLnkGre *a, const NMPlatformLnkGre *b)
 }
 
 void
+nm_platform_lnk_geneve_hash_update(const NMPlatformLnkGeneve *obj, NMHashState *h)
+{
+    nm_hash_update_vals(h, obj->id, obj->remote, obj->dst_port, obj->ttl, obj->tos, obj->df);
+    nm_hash_update_mem(h, &obj->remote6, sizeof(obj->remote6));
+}
+
+int
+nm_platform_lnk_geneve_cmp(const NMPlatformLnkGeneve *a, const NMPlatformLnkGeneve *b)
+{
+    NM_CMP_SELF(a, b);
+    NM_CMP_FIELD(a, b, id);
+    NM_CMP_FIELD(a, b, remote);
+    NM_CMP_FIELD_MEMCMP(a, b, remote6);
+    NM_CMP_FIELD(a, b, ttl);
+    NM_CMP_FIELD(a, b, tos);
+    NM_CMP_FIELD(a, b, dst_port);
+    NM_CMP_FIELD(a, b, df);
+    return 0;
+}
+
+void
 nm_platform_lnk_hsr_hash_update(const NMPlatformLnkHsr *obj, NMHashState *h)
 {
     nm_hash_update_vals(h,
@@ -8931,7 +9013,11 @@ nm_platform_ip4_rt_nexthop_hash_update(const NMPlatformIP4RtNextHop *obj,
     nm_assert(obj);
 
     w = for_id ? NM_MAX(obj->weight, 1u) : obj->weight;
-    nm_hash_update_vals(h, obj->ifindex, obj->gateway, w);
+    nm_hash_update_vals(h,
+                        obj->ifindex,
+                        obj->gateway,
+                        w,
+                        for_id ? (obj->rtnh_flags & RTNH_F_ONLINK) : obj->rtnh_flags);
 }
 
 void
@@ -8968,7 +9054,6 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                 obj->initrwnd,
                                 obj->mtu,
                                 obj->rto_min,
-                                obj->r_rtm_flags & RTNH_F_ONLINK,
                                 NM_HASH_COMBINE_BOOLS(guint16,
                                                       obj->quickack,
                                                       obj->lock_window,
@@ -8986,7 +9071,8 @@ nm_platform_ip4_route_hash_update(const NMPlatformIP4Route *obj,
                                     obj->via.addr_family == AF_INET6 ? obj->via.addr.addr6
                                                                      : in6addr_any,
                                     obj->gateway,
-                                    _ip4_route_weight_normalize(n_nexthops, obj->weight, FALSE));
+                                    _ip4_route_weight_normalize(n_nexthops, obj->weight, FALSE),
+                                    obj->r_rtm_flags & RTNH_F_ONLINK);
             }
         }
         break;
@@ -9079,6 +9165,11 @@ nm_platform_ip4_rt_nexthop_cmp(const NMPlatformIP4RtNextHop *a,
     w_b = for_id ? NM_MAX(b->weight, 1u) : b->weight;
     NM_CMP_DIRECT(w_a, w_b);
 
+    if (for_id)
+        NM_CMP_DIRECT(a->rtnh_flags & RTNH_F_ONLINK, b->rtnh_flags & RTNH_F_ONLINK);
+    else
+        NM_CMP_FIELD(a, b, rtnh_flags);
+
     return 0;
 }
 
@@ -9119,12 +9210,6 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
             NM_CMP_FIELD(a, b, mtu);
             NM_CMP_FIELD(a, b, rto_min);
 
-            /* Note that for NetworkManager, the onlink flag is only part of the entire route.
-             * For kernel, each next hop has it's own onlink flag (rtnh_flags). This means,
-             * we can only merge ECMP routes, if they agree with their onlink flag, and then
-             * all next hops are onlink (or not). */
-            NM_CMP_DIRECT(a->r_rtm_flags & RTNH_F_ONLINK, b->r_rtm_flags & RTNH_F_ONLINK);
-
             NM_CMP_FIELD_UNSAFE(a, b, quickack);
             NM_CMP_FIELD_UNSAFE(a, b, lock_window);
             NM_CMP_FIELD_UNSAFE(a, b, lock_cwnd);
@@ -9143,6 +9228,10 @@ nm_platform_ip4_route_cmp(const NMPlatformIP4Route *a,
                 NM_CMP_DIRECT(n_nexthops, nm_platform_ip4_route_get_n_nexthops(b));
                 NM_CMP_DIRECT(_ip4_route_weight_normalize(n_nexthops, a->weight, FALSE),
                               _ip4_route_weight_normalize(n_nexthops, b->weight, FALSE));
+                /* The onlink flag is per-nexthop. For the first nexthop it is in
+                 * r_rtm_flags. For extra nexthops, it's compared via
+                 * nm_platform_ip4_rt_nexthop_cmp(). */
+                NM_CMP_DIRECT(a->r_rtm_flags & RTNH_F_ONLINK, b->r_rtm_flags & RTNH_F_ONLINK);
             }
         }
         break;
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 033dc515..ba37d3a2 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -287,7 +287,7 @@ guint _nm_platform_signal_id_get(NMPlatformSignalIdType signal_type);
 #define __NMPlatformIPRoute_COMMON                                                        \
     __NMPlatformObjWithIfindex_COMMON;                                                    \
                                                                                           \
-    /* rtnh_flags
+    /* rtm_flags and rtnh_flags
      *
      * Routes with rtm_flags RTM_F_CLONED are hidden by platform and
      * do not exist from the point-of-view of platform users.
@@ -295,8 +295,15 @@ guint _nm_platform_signal_id_get(NMPlatformSignalIdType signal_type);
      *
      * NOTE: currently we ignore all flags except RTM_F_CLONED
      * and RTNH_F_ONLINK.
-     * We also may not properly consider the flags as part of the ID
-     * in route-cmp. */                                                                         \
+     *
+     * For IPv4 routes, the RTNH_F_ONLINK flag here applies to the
+     * first nexthop (which is embedded in the route struct). Extra
+     * nexthops (NMPlatformIP4RtNextHop) each have their own
+     * rtnh_flags field.
+     *
+     * For single-hop routes, this field comes directly from
+     * rtm_flags. For multi-hop routes from kernel, the first
+     * nexthop's RTNH_F_ONLINK from rtnh_flags is merged here. */                                                           \
     unsigned r_rtm_flags;                                                                 \
                                                                                           \
     /* RTA_METRICS.RTAX_ADVMSS (iproute2: advmss) */                                      \
@@ -733,10 +740,10 @@ typedef struct {
      */
     guint16 weight;
 
-    /* FIXME: each next hop in kernel also has a rtnh_flags (for example to
-     * set RTNH_F_ONLINK). As the next hop is part of the identifier of an
-     * IPv4 route, so is their flags. We must also track the flag, otherwise
-     * two routes that look different for kernel, get merged by platform cache. */
+    /* Each next hop in kernel has its own rtnh_flags (for example to
+     * set RTNH_F_ONLINK). The flags are part of the identifier of a
+     * route. */
+    unsigned rtnh_flags;
 } NMPlatformIP4RtNextHop;
 
 typedef struct {
@@ -836,6 +843,16 @@ typedef struct {
 } _nm_alignas(NMPlatformObject) NMPlatformLnkBond;
 
 typedef struct {
+    struct in6_addr remote6;
+    in_addr_t       remote;
+    guint32         id;
+    gint32          ttl;
+    guint16         dst_port;
+    guint8          tos;
+    guint8          df;
+} _nm_alignas(NMPlatformObject) NMPlatformLnkGeneve;
+
+typedef struct {
     int       parent_ifindex;
     in_addr_t local;
     in_addr_t remote;
@@ -1859,6 +1876,15 @@ nm_platform_link_vxlan_add(NMPlatform               *self,
 }
 
 static inline int
+nm_platform_link_geneve_add(NMPlatform                *self,
+                            const char                *name,
+                            const NMPlatformLnkGeneve *props,
+                            const NMPlatformLink     **out_link)
+{
+    return nm_platform_link_add(self, NM_LINK_TYPE_GENEVE, name, 0, NULL, 0, 0, props, out_link);
+}
+
+static inline int
 nm_platform_link_6lowpan_add(NMPlatform            *self,
                              const char            *name,
                              int                    parent,
@@ -2143,6 +2169,8 @@ const NMPlatformLnkBond *
 nm_platform_link_get_lnk_bond(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkBridge *
 nm_platform_link_get_lnk_bridge(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
+const NMPlatformLnkGeneve *
+nm_platform_link_get_lnk_geneve(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkGre *
 nm_platform_link_get_lnk_gre(NMPlatform *self, int ifindex, const NMPlatformLink **out_link);
 const NMPlatformLnkGre *
@@ -2482,6 +2510,7 @@ gboolean nm_platform_tc_sync(NMPlatform *self,
 const char *nm_platform_link_to_string(const NMPlatformLink *link, char *buf, gsize len);
 const char *nm_platform_lnk_bond_to_string(const NMPlatformLnkBond *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_bridge_to_string(const NMPlatformLnkBridge *lnk, char *buf, gsize len);
+const char *nm_platform_lnk_geneve_to_string(const NMPlatformLnkGeneve *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_gre_to_string(const NMPlatformLnkGre *lnk, char *buf, gsize len);
 const char *nm_platform_lnk_hsr_to_string(const NMPlatformLnkHsr *lnk, char *buf, gsize len);
 const char *
@@ -2537,6 +2566,7 @@ nm_platform_mptcp_addr_to_string(const NMPlatformMptcpAddr *mptcp_addr, char *bu
 int nm_platform_link_cmp(const NMPlatformLink *a, const NMPlatformLink *b);
 int nm_platform_lnk_bond_cmp(const NMPlatformLnkBond *a, const NMPlatformLnkBond *b);
 int nm_platform_lnk_bridge_cmp(const NMPlatformLnkBridge *a, const NMPlatformLnkBridge *b);
+int nm_platform_lnk_geneve_cmp(const NMPlatformLnkGeneve *a, const NMPlatformLnkGeneve *b);
 int nm_platform_lnk_gre_cmp(const NMPlatformLnkGre *a, const NMPlatformLnkGre *b);
 int nm_platform_lnk_hsr_cmp(const NMPlatformLnkHsr *a, const NMPlatformLnkHsr *b);
 int nm_platform_lnk_infiniband_cmp(const NMPlatformLnkInfiniband *a,
@@ -2612,6 +2642,7 @@ void nm_platform_routing_rule_hash_update(const NMPlatformRoutingRule *obj,
                                           NMHashState                 *h);
 void nm_platform_lnk_bond_hash_update(const NMPlatformLnkBond *obj, NMHashState *h);
 void nm_platform_lnk_bridge_hash_update(const NMPlatformLnkBridge *obj, NMHashState *h);
+void nm_platform_lnk_geneve_hash_update(const NMPlatformLnkGeneve *obj, NMHashState *h);
 void nm_platform_lnk_gre_hash_update(const NMPlatformLnkGre *obj, NMHashState *h);
 void nm_platform_lnk_hsr_hash_update(const NMPlatformLnkHsr *obj, NMHashState *h);
 void nm_platform_lnk_infiniband_hash_update(const NMPlatformLnkInfiniband *obj, NMHashState *h);
diff --git a/src/libnm-platform/nmp-base.h b/src/libnm-platform/nmp-base.h
index f6656296..ae538f5b 100644
--- a/src/libnm-platform/nmp-base.h
+++ b/src/libnm-platform/nmp-base.h
@@ -166,6 +166,7 @@ typedef enum _nm_packed {
     NMP_OBJECT_TYPE_TFILTER,
 
     NMP_OBJECT_TYPE_LNK_BRIDGE,
+    NMP_OBJECT_TYPE_LNK_GENEVE,
     NMP_OBJECT_TYPE_LNK_GRE,
     NMP_OBJECT_TYPE_LNK_GRETAP,
     NMP_OBJECT_TYPE_LNK_HSR,
@@ -194,15 +195,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;
 }
 
 /*****************************************************************************/
diff --git a/src/libnm-platform/nmp-object.c b/src/libnm-platform/nmp-object.c
index 56533c99..d73eb641 100644
--- a/src/libnm-platform/nmp-object.c
+++ b/src/libnm-platform/nmp-object.c
@@ -3469,6 +3469,18 @@ const NMPClass _nmp_classes[NMP_OBJECT_TYPE_MAX] = {
             .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_bridge_hash_update,
             .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_bridge_cmp,
         },
+    [NMP_OBJECT_TYPE_LNK_GENEVE - 1] =
+        {
+            .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
+            .obj_type              = NMP_OBJECT_TYPE_LNK_GENEVE,
+            .sizeof_data           = sizeof(NMPObjectLnkGeneve),
+            .sizeof_public         = sizeof(NMPlatformLnkGeneve),
+            .obj_type_name         = "geneve",
+            .lnk_link_type         = NM_LINK_TYPE_GENEVE,
+            .cmd_plobj_to_string   = (CmdPlobjToStringFunc) nm_platform_lnk_geneve_to_string,
+            .cmd_plobj_hash_update = (CmdPlobjHashUpdateFunc) nm_platform_lnk_geneve_hash_update,
+            .cmd_plobj_cmp         = (CmdPlobjCmpFunc) nm_platform_lnk_geneve_cmp,
+        },
     [NMP_OBJECT_TYPE_LNK_GRE - 1] =
         {
             .parent                = DEDUP_MULTI_OBJ_CLASS_INIT(),
diff --git a/src/libnm-platform/nmp-object.h b/src/libnm-platform/nmp-object.h
index b526fc3b..a960c528 100644
--- a/src/libnm-platform/nmp-object.h
+++ b/src/libnm-platform/nmp-object.h
@@ -251,6 +251,10 @@ typedef struct {
 } NMPObjectLnkBond;
 
 typedef struct {
+    NMPlatformLnkGeneve _public;
+} NMPObjectLnkGeneve;
+
+typedef struct {
     NMPlatformLnkGre _public;
 } NMPObjectLnkGre;
 
@@ -383,6 +387,9 @@ struct _NMPObject {
         NMPlatformLnkBond lnk_bond;
         NMPObjectLnkBond  _lnk_bond;
 
+        NMPlatformLnkGeneve lnk_geneve;
+        NMPObjectLnkGeneve  _lnk_geneve;
+
         NMPlatformLnkGre lnk_gre;
         NMPObjectLnkGre  _lnk_gre;
 
@@ -543,6 +550,7 @@ _NMP_OBJECT_TYPE_IS_OBJ_WITH_IFINDEX(NMPObjectType obj_type)
 
     case NMP_OBJECT_TYPE_LNK_BRIDGE:
     case NMP_OBJECT_TYPE_LNK_BOND:
+    case NMP_OBJECT_TYPE_LNK_GENEVE:
     case NMP_OBJECT_TYPE_LNK_GRE:
     case NMP_OBJECT_TYPE_LNK_GRETAP:
     case NMP_OBJECT_TYPE_LNK_HSR:
diff --git a/src/libnm-platform/tests/test-nm-platform.c b/src/libnm-platform/tests/test-nm-platform.c
index 22148f8c..d087d66f 100644
--- a/src/libnm-platform/tests/test-nm-platform.c
+++ b/src/libnm-platform/tests/test-nm-platform.c
@@ -19,6 +19,7 @@ G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectIP6Route))
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLink));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkBond));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkBridge));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkGeneve));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkGre));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkInfiniband));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPObjectLnkIp6Tnl));
@@ -49,6 +50,7 @@ G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformIPXRoute)
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLink));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkBond));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkBridge));
+G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkGeneve));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkGre));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkInfiniband));
 G_STATIC_ASSERT(_nm_alignof(NMPlatformObject) == _nm_alignof(NMPlatformLnkIp6Tnl));