about summary refs log tree commit diff
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
parent067fb576988f685e83ac8b0ae690334aff547c85 (diff)
New upstream version 1.56.1 upstream/1.56.1
-rw-r--r--NEWS10
-rw-r--r--data/meson.build11
-rw-r--r--data/org.freedesktop.NetworkManager.rules.in17
-rw-r--r--docs/api/network-manager-docs.xml1
-rw-r--r--docs/libnm/libnm-docs.xml2
-rw-r--r--introspection/meson.build1
-rw-r--r--introspection/org.freedesktop.NetworkManager.Device.Geneve.xml63
-rw-r--r--introspection/org.freedesktop.NetworkManager.Device.xml17
-rw-r--r--man/nmcli.xml16
-rw-r--r--meson.build9
-rw-r--r--meson_options.txt1
-rw-r--r--po/POTFILES.in4
-rw-r--r--src/core/NetworkManagerUtils.c9
-rw-r--r--src/core/devices/nm-device-factory.c1
-rw-r--r--src/core/devices/nm-device-geneve.c487
-rw-r--r--src/core/devices/nm-device-geneve.h33
-rw-r--r--src/core/devices/nm-device.c246
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c8
-rw-r--r--src/core/dns/nm-dns-manager.c4
-rw-r--r--src/core/meson.build1
-rw-r--r--src/core/nm-checkpoint.c75
-rw-r--r--src/core/nm-config-data.c50
-rw-r--r--src/core/nm-config.c204
-rw-r--r--src/core/nm-config.h11
-rw-r--r--src/core/nm-ip-config.c8
-rw-r--r--src/core/nm-ip-config.h2
-rw-r--r--src/core/nm-l3-config-data.c9
-rw-r--r--src/core/nm-l3cfg.c4
-rw-r--r--src/core/nm-l3cfg.h6
-rw-r--r--src/core/nm-netns.c8
-rw-r--r--src/core/platform/tests/test-common.c63
-rw-r--r--src/core/platform/tests/test-common.h8
-rw-r--r--src/core/platform/tests/test-link.c64
-rw-r--r--src/core/platform/tests/test-route.c74
-rw-r--r--src/core/settings/nm-agent-manager.c35
-rw-r--r--src/core/settings/nm-settings-connection.c3
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c2
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c7
-rw-r--r--src/core/tests/config/test-config.c103
-rw-r--r--src/libnm-base/nm-config-base.h3
-rw-r--r--src/libnm-client-impl/libnm.ver25
-rw-r--r--src/libnm-client-impl/meson.build1
-rw-r--r--src/libnm-client-impl/nm-client.c1
-rw-r--r--src/libnm-client-impl/nm-device-geneve.c344
-rw-r--r--src/libnm-client-impl/nm-device.c73
-rw-r--r--src/libnm-client-impl/nm-libnm-utils.c1
-rw-r--r--src/libnm-client-impl/nm-libnm-utils.h3
-rw-r--r--src/libnm-client-public/NetworkManager.h2
-rw-r--r--src/libnm-client-public/meson.build1
-rw-r--r--src/libnm-client-public/nm-autoptr.h2
-rw-r--r--src/libnm-client-public/nm-device-geneve.h60
-rw-r--r--src/libnm-client-public/nm-device.h9
-rw-r--r--src/libnm-core-aux-intern/nm-libnm-core-utils.c6
-rw-r--r--src/libnm-core-impl/gen-metadata-nm-settings-libnm-core.xml.in28
-rw-r--r--src/libnm-core-impl/meson.build1
-rw-r--r--src/libnm-core-impl/nm-connection.c17
-rw-r--r--src/libnm-core-impl/nm-meta-setting-base-impl.c9
-rw-r--r--src/libnm-core-impl/nm-setting-bridge.c14
-rw-r--r--src/libnm-core-impl/nm-setting-geneve.c354
-rw-r--r--src/libnm-core-impl/nm-setting-ip-config.c3
-rw-r--r--src/libnm-core-impl/nm-setting.c2
-rw-r--r--src/libnm-core-intern/nm-core-internal.h1
-rw-r--r--src/libnm-core-intern/nm-meta-setting-base-impl.h1
-rw-r--r--src/libnm-core-public/meson.build1
-rw-r--r--src/libnm-core-public/nm-connection.h6
-rw-r--r--src/libnm-core-public/nm-core-types.h1
-rw-r--r--src/libnm-core-public/nm-dbus-interface.h51
-rw-r--r--src/libnm-core-public/nm-setting-geneve.h74
-rw-r--r--src/libnm-core-public/nm-version-macros.h.in1
-rw-r--r--src/libnm-core-public/nm-version.h6
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h1
-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
-rw-r--r--src/libnm-systemd-shared/src/basic/sort-util.h7
-rw-r--r--src/libnm-systemd-shared/src/basic/string-util.c4
-rw-r--r--src/libnm-systemd-shared/src/basic/string-util.h18
-rw-r--r--src/libnm-systemd-shared/src/fundamental/macro-fundamental.h9
-rw-r--r--src/libnm-udev-aux/nm-udev-utils.c15
-rw-r--r--src/libnmc-setting/nm-meta-setting-base-impl.c9
-rw-r--r--src/libnmc-setting/nm-meta-setting-base-impl.h1
-rw-r--r--src/libnmc-setting/nm-meta-setting-desc.c60
-rw-r--r--src/libnmc-setting/settings-docs.h.in6
-rw-r--r--src/nm-initrd-generator/nmi-cmdline-reader.c6
-rw-r--r--src/nmcli/connections.c18
-rw-r--r--src/nmcli/devices.c214
-rw-r--r--src/nmcli/gen-metadata-nm-settings-nmcli.xml.in32
-rw-r--r--vapi/NM-1.0.metadata2
92 files changed, 3291 insertions, 203 deletions
diff --git a/NEWS b/NEWS
index 3ee8e88a..109688ad 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,14 @@
 =============================================
+NetworkManager-1.56.1
+Overview of changes since NetworkManager-1.56
+=============================================
+
+* Add support for GENEVE interface.
+* Allow persisting the managed state across reboots from nmcli and the D-Bus API.
+* Allow changing the device's administrative state in the kernel at the same
+  time as a change to the managed state from nmcli and the D-Bus API.
+
+=============================================
 NetworkManager-1.56
 Overview of changes since NetworkManager-1.54
 =============================================
diff --git a/data/meson.build b/data/meson.build
index b77bf434..34c98e3b 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -68,8 +68,17 @@ if enable_polkit
     output: '@BASENAME@',
     po_dir: po_dir,
     install: true,
-    install_dir: polkit_gobject_policydir,
+    install_dir: polkit_policydir,
   )
+
+  if polkit_noauth_group != ''
+    configure_file(
+      input: 'org.freedesktop.NetworkManager.rules.in',
+      output: '@BASENAME@',
+      install_dir: polkit_rulesdir,
+      configuration: {'NM_POLKIT_NOAUTH_GROUP': polkit_noauth_group},
+    )
+  endif
 endif
 
 if enable_firewalld_zone
diff --git a/data/org.freedesktop.NetworkManager.rules.in b/data/org.freedesktop.NetworkManager.rules.in
new file mode 100644
index 00000000..d6df0b32
--- /dev/null
+++ b/data/org.freedesktop.NetworkManager.rules.in
@@ -0,0 +1,17 @@
+// NetworkManager authorizations/policy for the @NM_POLKIT_NOAUTH_GROUP@ group.
+//
+// DO NOT EDIT THIS FILE, it will be overwritten on update.
+//
+// Allow users in the @NM_POLKIT_NOAUTH_GROUP@ group to create system-wide connections without being
+// prompted for a password if they are in a local console.
+// This is optional and is only recommended to maintain backwards compatibility
+// in systems where it was already working in this way. It is discouraged
+// otherwise.
+
+polkit.addRule(function(action, subject) {
+    if (action.id == "org.freedesktop.NetworkManager.settings.modify.system" &&
+        subject.isInGroup("@NM_POLKIT_NOAUTH_GROUP@") &&
+        subject.local) {
+        return polkit.Result.YES;
+    }
+});
diff --git a/docs/api/network-manager-docs.xml b/docs/api/network-manager-docs.xml
index caff6fa4..99164273 100644
--- a/docs/api/network-manager-docs.xml
+++ b/docs/api/network-manager-docs.xml
@@ -183,6 +183,7 @@
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Bridge.xml"/>
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Dummy.xml"/>
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Generic.xml"/>
+      <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Geneve.xml"/>
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Hsr.xml"/>
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.IPTunnel.xml"/>
       <xi:include href="dbus-org.freedesktop.NetworkManager.Device.Infiniband.xml"/>
diff --git a/docs/libnm/libnm-docs.xml b/docs/libnm/libnm-docs.xml
index 39f62417..719d8998 100644
--- a/docs/libnm/libnm-docs.xml
+++ b/docs/libnm/libnm-docs.xml
@@ -317,6 +317,7 @@ print ("NetworkManager version " + client.get_version())]]></programlisting></in
     <xi:include href="xml/nm-setting-dummy.xml"/>
     <xi:include href="xml/nm-setting-ethtool.xml"/>
     <xi:include href="xml/nm-setting-generic.xml"/>
+    <xi:include href="xml/nm-setting-geneve.xml"/>
     <xi:include href="xml/nm-setting-gsm.xml"/>
     <xi:include href="xml/nm-setting-hostname.xml"/>
     <xi:include href="xml/nm-setting-hsr.xml"/>
@@ -377,6 +378,7 @@ print ("NetworkManager version " + client.get_version())]]></programlisting></in
     <xi:include href="xml/nm-device-dummy.xml"/>
     <xi:include href="xml/nm-device-ethernet.xml"/>
     <xi:include href="xml/nm-device-generic.xml"/>
+    <xi:include href="xml/nm-device-geneve.xml"/>
     <xi:include href="xml/nm-device-hsr.xml"/>
     <xi:include href="xml/nm-device-infiniband.xml"/>
     <xi:include href="xml/nm-device-ip-tunnel.xml"/>
diff --git a/introspection/meson.build b/introspection/meson.build
index 16bb5ed4..7703b469 100644
--- a/introspection/meson.build
+++ b/introspection/meson.build
@@ -15,6 +15,7 @@ ifaces = [
   'org.freedesktop.NetworkManager.Device.Bridge',
   'org.freedesktop.NetworkManager.Device.Dummy',
   'org.freedesktop.NetworkManager.Device.Generic',
+  'org.freedesktop.NetworkManager.Device.Geneve',
   'org.freedesktop.NetworkManager.Device.Hsr',
   'org.freedesktop.NetworkManager.Device.IPTunnel',
   'org.freedesktop.NetworkManager.Device.Infiniband',
diff --git a/introspection/org.freedesktop.NetworkManager.Device.Geneve.xml b/introspection/org.freedesktop.NetworkManager.Device.Geneve.xml
new file mode 100644
index 00000000..fc568dff
--- /dev/null
+++ b/introspection/org.freedesktop.NetworkManager.Device.Geneve.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<node name="/">
+  <!--
+      org.freedesktop.NetworkManager.Device.Geneve:
+      @short_description: GENEVE Device.
+
+  -->
+  <interface name="org.freedesktop.NetworkManager.Device.Geneve">
+
+    <!--
+        Id:
+        @since: 1.56.1
+
+        The GENEVE Virtual Network Identifier (VNI).
+    -->
+    <property name="Id" type="u" access="read"/>
+
+    <!--
+        Remote:
+        @since: 1.56.1
+
+        The IP (v4 or v6) address of the remote endpoint to which GENEVE packets
+        are sent.
+    -->
+    <property name="Remote" type="s" access="read"/>
+
+    <!--
+        Tos:
+        @since: 1.56.1
+
+        The value to use in the IP ToS field for GENEVE packets sent to the remote
+        endpoint.
+    -->
+    <property name="Tos" type="y" access="read"/>
+
+    <!--
+        Ttl:
+        @since: 1.56.1
+
+        The value to use in the IP TTL field for GENEVE packets sent to the remote
+        endpoint.
+    -->
+    <property name="Ttl" type="i" access="read"/>
+
+    <!--
+        Df:
+        @since: 1.56.1
+
+        The Don't Fragment (DF) flag setting for GENEVE packets. 0 means unset,
+        1 means set, 2 means inherit from the underlying interface.
+    -->
+    <property name="Df" type="y" access="read"/>
+
+    <!--
+        DstPort:
+        @since: 1.56.1
+
+        Destination port for outgoing GENEVE packets.
+    -->
+    <property name="DstPort" type="q" access="read"/>
+
+  </interface>
+</node>
diff --git a/introspection/org.freedesktop.NetworkManager.Device.xml b/introspection/org.freedesktop.NetworkManager.Device.xml
index 085d2a1f..d770cfe6 100644
--- a/introspection/org.freedesktop.NetworkManager.Device.xml
+++ b/introspection/org.freedesktop.NetworkManager.Device.xml
@@ -175,6 +175,9 @@
         property has a similar effect to configuring the device as unmanaged via
         the keyfile.unmanaged-devices setting in NetworkManager.conf. Changes to
         this value are not persistent and lost after NetworkManager restart.
+
+        DEPRECATED: 1.56.1: Use the SetManaged method instead, which supports
+        additional features like persisting the state to disk
     -->
     <property name="Managed" type="b" access="readwrite"/>
 
@@ -392,6 +395,20 @@
     <method name="Delete"/>
 
     <!--
+        SetManaged:
+        @managed:(<link linkend="NMDeviceManaged">NMDeviceManaged</link>) Whether the device is managed. Possible values are "no" (0), "yes" (1) and "reset" (2).
+        @flags: (<link linkend="NMDeviceManagedFlags">NMDeviceManagedFlags</link>) flags.
+        @since: 1.56.1
+
+        Set the managed state of the device. With the flags argument different
+        behaviors can be achieved, like storing the new managed state to disk.
+    -->
+    <method name="SetManaged">
+      <arg name="managed" type="u" direction="in"/>
+      <arg name="flags" type="u" direction="in"/>
+      </method>
+
+    <!--
         StateChanged:
         @new_state: (<link linkend="NMDeviceState">NMDeviceState</link>) The new state of the device.
         @old_state: (<link linkend="NMDeviceState">NMDeviceState</link>) The previous state of the device.
diff --git a/man/nmcli.xml b/man/nmcli.xml
index 7bce0de7..94f4c64c 100644
--- a/man/nmcli.xml
+++ b/man/nmcli.xml
@@ -1436,15 +1436,31 @@
           </arg>
           <arg>
             <option>managed</option>
+            <group>
+              <arg choice='plain'>--permanent</arg>
+              <arg choice='plain'>--permanent-only</arg>
+            </group>
             <group choice='req'>
               <arg choice='plain'>yes</arg>
               <arg choice='plain'>no</arg>
+              <arg choice='plain'>up</arg>
+              <arg choice='plain'>down</arg>
+              <arg choice='plain'>reset</arg>
             </group>
           </arg>
         </term>
 
         <listitem>
           <para>Set device properties.</para>
+
+          <para>The <option>managed</option> property accepts a <option>--permanent</option>
+          option to persist the managed state to disk, and not only in runtime. With
+          <option>--permanent-only</option> only the permanent managed state is set, and not the
+          runtime managed state. The special values <option>up</option> and <option>down</option>
+          can be used to set the administrative state of the device at the same time as the runtime
+          managed state. The <option>reset</option> value clears the explicit managed setting, and
+          with <option>--permanent</option> or <option>--permanent-only</option> it also removes
+          the persisted managed setting.</para>
         </listitem>
       </varlistentry>
 
diff --git a/meson.build b/meson.build
index 0657ce5a..3868df6e 100644
--- a/meson.build
+++ b/meson.build
@@ -5,7 +5,7 @@ project(
 # NOTE: When incrementing version also add corresponding
 #       NM_VERSION_x_y_z macros in
 #       "src/libnm-core-public/nm-version-macros.h.in"
-  version: '1.56.0',
+  version: '1.56.1',
   license: 'GPL2+',
   default_options: [
     'buildtype=debugoptimized',
@@ -526,7 +526,8 @@ config_h.set10('WITH_TEAMDCTL', enable_teamdctl)
 enable_polkit = get_option('polkit')
 if enable_polkit
   # FIXME: policydir should be relative to `datadir`, not `prefix`. Fixed in https://gitlab.freedesktop.org/polkit/polkit/merge_requests/2
-  polkit_gobject_policydir = dependency('polkit-gobject-1').get_variable(pkgconfig: 'policydir', pkgconfig_define: ['prefix', nm_prefix])
+  polkit_policydir = dependency('polkit-gobject-1').get_variable(pkgconfig: 'policydir', pkgconfig_define: ['prefix', nm_prefix])
+  polkit_rulesdir = join_paths(fs.parent(polkit_policydir), 'rules.d')
 endif
 
 config_auth_polkit_default = get_option('config_auth_polkit_default')
@@ -537,6 +538,8 @@ config_h.set_quoted('NM_CONFIG_DEFAULT_MAIN_AUTH_POLKIT', config_auth_polkit_def
 
 enable_modify_system = get_option('modify_system')
 
+polkit_noauth_group = get_option('polkit_noauth_group')
+
 polkit_agent_helper_1_path = get_option('polkit_agent_helper_1')
 foreach p : [ '/usr/libexec/polkit-agent-helper-1',
               '/usr/lib/polkit-1/polkit-agent-helper-1',
@@ -1098,7 +1101,7 @@ output += '  dbus_conf_dir: ' + dbus_conf_dir + '\n'
 output += '\nPlatform:\n'
 output += '  session tracking: ' + ','.join(session_trackers) + '\n'
 output += '  suspend/resume: ' + suspend_resume + '\n'
-output += '  policykit: ' + enable_polkit.to_string() + ' (default: ' + config_auth_polkit_default + ')'
+output += '  policykit: ' + enable_polkit.to_string() + ' (default: ' + config_auth_polkit_default + ', noauth_group: "' + polkit_noauth_group + '")'
 if enable_polkit
   output += ' ('
   if enable_modify_system
diff --git a/meson_options.txt b/meson_options.txt
index d28cc76f..631b3037 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -19,6 +19,7 @@ option('suspend_resume', type: 'combo', choices: ['systemd', 'elogind', 'console
 option('polkit', type: 'boolean', value: true, description: 'User auth-polkit configuration option.')
 option('config_auth_polkit_default', type: 'combo', choices: ['default', 'true', 'false', 'root-only'], value: 'default', description: 'Default value for configuration main.auth-polkit.')
 option('modify_system', type: 'boolean', value: false, description: 'Allow users to modify system connections')
+option('polkit_noauth_group', type: 'string', value: '', description: 'Allow users of the selected group, typically sudo or wheel, to modify system connections without introducing a password (discouraged)')
 option('polkit_agent_helper_1', type: 'string', value: '', description: 'Path name to the polkit-agent-helper-1 binary from polkit')
 option('selinux', type: 'boolean', value: true, description: 'Build with SELinux')
 option('systemd_journal', type: 'boolean', value: true, description: 'Use systemd journal for logging')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index feeaf9eb..04de8d3f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,12 +5,14 @@ src/core/NetworkManagerUtils.c
 src/core/devices/adsl/nm-device-adsl.c
 src/core/devices/bluetooth/nm-bluez-manager.c
 src/core/devices/bluetooth/nm-device-bt.c
+src/core/devices/nm-device.c
 src/core/devices/nm-device-6lowpan.c
 src/core/devices/nm-device-bond.c
 src/core/devices/nm-device-bridge.c
 src/core/devices/nm-device-dummy.c
 src/core/devices/nm-device-ethernet-utils.c
 src/core/devices/nm-device-ethernet.c
+src/core/devices/nm-device-geneve.c
 src/core/devices/nm-device-infiniband.c
 src/core/devices/nm-device-ip-tunnel.c
 src/core/devices/nm-device-loopback.c
@@ -46,6 +48,7 @@ src/libnm-client-impl/nm-device-bt.c
 src/libnm-client-impl/nm-device-dummy.c
 src/libnm-client-impl/nm-device-ethernet.c
 src/libnm-client-impl/nm-device-generic.c
+src/libnm-client-impl/nm-device-geneve.c
 src/libnm-client-impl/nm-device-hsr.c
 src/libnm-client-impl/nm-device-infiniband.c
 src/libnm-client-impl/nm-device-ip-tunnel.c
@@ -90,6 +93,7 @@ src/libnm-core-impl/nm-setting-connection.c
 src/libnm-core-impl/nm-setting-dcb.c
 src/libnm-core-impl/nm-setting-ethtool.c
 src/libnm-core-impl/nm-setting-generic.c
+src/libnm-core-impl/nm-setting-geneve.c
 src/libnm-core-impl/nm-setting-gsm.c
 src/libnm-core-impl/nm-setting-hsr.c
 src/libnm-core-impl/nm-setting-infiniband.c
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index 32df3d6d..95ba20f1 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -1495,11 +1495,10 @@ nm_utils_ip_route_attribute_to_platform(int                addr_family,
         r4->scope_inv = nm_platform_route_scope_inv(scope);
     }
 
-    /* Note that for IPv4 routes in kernel, the onlink flag can be set for
-     * each next hop separately (rtnh_flags). Not for NetworkManager. We can
-     * only merge routes as ECMP routes (when setting a weight) if they all
-     * share the same onlink flag. See NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID.
-     * That simplifies the code. */
+    /* For IPv4 routes in kernel, the onlink flag is per-nexthop (rtnh_flags).
+     * Here we set the flag on r_rtm_flags which represents the first nexthop's
+     * flags. For ECMP routes, each nexthop carries its own onlink flag, so
+     * routes with different onlink settings per-nexthop can be merged. */
     GET_ATTR(NM_IP_ROUTE_ATTRIBUTE_ONLINK, onlink, BOOLEAN, boolean, FALSE);
     r->r_rtm_flags = ((onlink) ? (unsigned) RTNH_F_ONLINK : 0u);
 
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index 15858362..24755a5f 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -412,6 +412,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb
     _ADD_INTERNAL(nm_dummy_device_factory_get_type);
     _ADD_INTERNAL(nm_ethernet_device_factory_get_type);
     _ADD_INTERNAL(nm_generic_device_factory_get_type);
+    _ADD_INTERNAL(nm_geneve_device_factory_get_type);
     _ADD_INTERNAL(nm_hsr_device_factory_get_type);
     _ADD_INTERNAL(nm_infiniband_device_factory_get_type);
     _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type);
diff --git a/src/core/devices/nm-device-geneve.c b/src/core/devices/nm-device-geneve.c
new file mode 100644
index 00000000..0968a2fb
--- /dev/null
+++ b/src/core/devices/nm-device-geneve.c
@@ -0,0 +1,487 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-manager.h"
+#include "nm-device-geneve.h"
+
+#include "libnm-core-intern/nm-core-internal.h"
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "nm-setting-geneve.h"
+#include "libnm-platform/nm-platform.h"
+#include "nm-device-factory.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceGeneve
+#include "nm-device-logging.h"
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceGeneve,
+                             PROP_ID,
+                             PROP_REMOTE,
+                             PROP_TOS,
+                             PROP_TTL,
+                             PROP_DF,
+                             PROP_DST_PORT, );
+
+typedef struct {
+    NMPlatformLnkGeneve props;
+} NMDeviceGenevePrivate;
+
+struct _NMDeviceGeneve {
+    NMDevice              parent;
+    NMDeviceGenevePrivate _priv;
+};
+
+struct _NMDeviceGeneveClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceGeneve, nm_device_geneve, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_GENEVE_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceGeneve, NM_IS_DEVICE_GENEVE, NMDevice)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceGeneve            *self;
+    NMDeviceGenevePrivate     *priv;
+    const NMPlatformLink      *plink;
+    const NMPlatformLnkGeneve *props;
+    int                        ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE_GENEVE(device));
+    self = NM_DEVICE_GENEVE(device);
+    priv = NM_DEVICE_GENEVE_GET_PRIVATE(self);
+
+    ifindex = nm_device_get_ifindex(device);
+    g_return_if_fail(ifindex > 0);
+    props = nm_platform_link_get_lnk_geneve(nm_device_get_platform(device), ifindex, &plink);
+
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get GENEVE properties");
+        return;
+    }
+
+    g_object_freeze_notify((GObject *) device);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+#define CHECK_PROPERTY_CHANGED_IN6ADDR(field, prop)                                 \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        if (memcmp(&priv->props.field, &props->field, sizeof(props->field)) != 0) { \
+            priv->props.field = props->field;                                       \
+            _notify(self, prop);                                                    \
+        }                                                                           \
+    }                                                                               \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(id, PROP_ID);
+    CHECK_PROPERTY_CHANGED(remote, PROP_REMOTE);
+    CHECK_PROPERTY_CHANGED_IN6ADDR(remote6, PROP_REMOTE);
+    CHECK_PROPERTY_CHANGED(tos, PROP_TOS);
+    CHECK_PROPERTY_CHANGED(ttl, PROP_TTL);
+    CHECK_PROPERTY_CHANGED(df, PROP_DF);
+    CHECK_PROPERTY_CHANGED(dst_port, PROP_DST_PORT);
+
+    g_object_thaw_notify((GObject *) device);
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_geneve_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NMDeviceGeneve        *self = NM_DEVICE_GENEVE(device);
+    NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(self);
+    guint                  i;
+
+    NM_DEVICE_CLASS(nm_device_geneve_parent_class)->unrealize_notify(device);
+
+    memset(&priv->props, 0, sizeof(NMPlatformLnkGeneve));
+
+    for (i = 1; i < _PROPERTY_ENUMS_LAST; i++)
+        g_object_notify_by_pspec(G_OBJECT(self), obj_properties[i]);
+}
+
+static gboolean
+create_and_realize(NMDevice              *device,
+                   NMConnection          *connection,
+                   NMDevice              *parent,
+                   const NMPlatformLink **out_plink,
+                   GError               **error)
+{
+    const char         *iface = nm_device_get_iface(device);
+    NMPlatformLnkGeneve props = {};
+    NMSettingGeneve    *s_geneve;
+    const char         *str;
+    int                 r;
+
+    s_geneve = nm_connection_get_setting_geneve(connection);
+    g_return_val_if_fail(s_geneve, FALSE);
+
+    props.id = nm_setting_geneve_get_id(s_geneve);
+
+    str = nm_setting_geneve_get_remote(s_geneve);
+    if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.remote)
+        && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.remote6)) {
+        return nm_assert_unreachable_val(FALSE);
+    }
+    props.tos      = nm_setting_geneve_get_tos(s_geneve);
+    props.ttl      = nm_setting_geneve_get_ttl(s_geneve);
+    props.df       = nm_setting_geneve_get_df(s_geneve);
+    props.dst_port = nm_setting_geneve_get_destination_port(s_geneve);
+
+    r = nm_platform_link_geneve_add(nm_device_get_platform(device), iface, &props, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create geneve interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6)
+{
+    NMIPAddr candidate_addr;
+    int      addr_family;
+
+    if (!candidate)
+        return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6);
+
+    if (!nm_inet_parse_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
+        return FALSE;
+
+    if (!nm_ip_addr_equal(addr_family,
+                          &candidate_addr,
+                          NM_IS_IPv4(addr_family) ? (gpointer) &addr4 : addr6))
+        return FALSE;
+
+    if (NM_IS_IPv4(addr_family))
+        return IN6_IS_ADDR_UNSPECIFIED(addr6);
+    else
+        return addr4 == 0u;
+}
+
+static gboolean
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
+{
+    NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(device);
+    NMSettingGeneve       *s_geneve;
+
+    if (!NM_DEVICE_CLASS(nm_device_geneve_parent_class)
+             ->check_connection_compatible(device, connection, check_properties, error))
+        return FALSE;
+
+    if (check_properties && nm_device_is_real(device)) {
+        s_geneve = nm_connection_get_setting_geneve(connection);
+
+        if (priv->props.id != nm_setting_geneve_get_id(s_geneve)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve id mismatches");
+            return FALSE;
+        }
+
+        if (!address_matches(nm_setting_geneve_get_remote(s_geneve),
+                             priv->props.remote,
+                             &priv->props.remote6)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve remote address mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.dst_port != nm_setting_geneve_get_destination_port(s_geneve)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve destination port mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.tos != nm_setting_geneve_get_tos(s_geneve)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve TOS mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.ttl != nm_setting_geneve_get_ttl(s_geneve)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve TTL mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.df != nm_setting_geneve_get_df(s_geneve)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "geneve DF mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice            *device,
+                    NMConnection        *connection,
+                    const char          *specific_object,
+                    NMConnection *const *existing_connections,
+                    GError             **error)
+{
+    NMSettingGeneve *s_geneve;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_GENEVE_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Geneve connection"),
+                              NULL,
+                              NULL);
+
+    s_geneve = nm_connection_get_setting_geneve(connection);
+    if (!s_geneve) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'geneve' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(device);
+    NMSettingGeneve *s_geneve   = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_GENEVE);
+    char             sbuf[NM_INET_ADDRSTRLEN];
+
+    if (priv->props.id != nm_setting_geneve_get_id(s_geneve))
+        g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_ID, priv->props.id, NULL);
+
+    /* Handle remote (IPv4 or IPv6) */
+    if (priv->props.remote) {
+        g_object_set(s_geneve,
+                     NM_SETTING_GENEVE_REMOTE,
+                     nm_inet4_ntop(priv->props.remote, sbuf),
+                     NULL);
+    } else if (memcmp(&priv->props.remote6, &in6addr_any, sizeof(in6addr_any))) {
+        g_object_set(s_geneve,
+                     NM_SETTING_GENEVE_REMOTE,
+                     nm_inet6_ntop(&priv->props.remote6, sbuf),
+                     NULL);
+    }
+
+    if (priv->props.dst_port != nm_setting_geneve_get_destination_port(s_geneve))
+        g_object_set(G_OBJECT(s_geneve),
+                     NM_SETTING_GENEVE_DESTINATION_PORT,
+                     priv->props.dst_port,
+                     NULL);
+
+    if (priv->props.tos != nm_setting_geneve_get_tos(s_geneve))
+        g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_TOS, priv->props.tos, NULL);
+
+    if (priv->props.ttl != nm_setting_geneve_get_ttl(s_geneve))
+        g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_TTL, priv->props.ttl, NULL);
+
+    if (priv->props.df != nm_setting_geneve_get_df(s_geneve))
+        g_object_set(G_OBJECT(s_geneve), NM_SETTING_GENEVE_DF, priv->props.df, NULL);
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_ID:
+        g_value_set_uint(value, priv->props.id);
+        break;
+    case PROP_REMOTE:
+        if (priv->props.remote)
+            g_value_take_string(value, nm_inet4_ntop_dup(priv->props.remote));
+        else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.remote6))
+            g_value_take_string(value, nm_inet6_ntop_dup(&priv->props.remote6));
+        break;
+    case PROP_TOS:
+        g_value_set_uchar(value, priv->props.tos);
+        break;
+    case PROP_TTL:
+        g_value_set_uchar(value, priv->props.ttl);
+        break;
+    case PROP_DF:
+        g_value_set_uint(value, priv->props.df);
+        break;
+    case PROP_DST_PORT:
+        g_value_set_uint(value, priv->props.dst_port);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_geneve_init(NMDeviceGeneve *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_geneve = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_GENEVE,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Id", "u", NM_DEVICE_GENEVE_ID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Remote", "s", NM_DEVICE_GENEVE_REMOTE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Tos", "y", NM_DEVICE_GENEVE_TOS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ttl", "y", NM_DEVICE_GENEVE_TTL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Df", "u", NM_DEVICE_GENEVE_DF),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DstPort",
+                                                           "q",
+                                                           NM_DEVICE_GENEVE_DST_PORT), ), ),
+};
+
+static void
+nm_device_geneve_class_init(NMDeviceGeneveClass *klass)
+{
+    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass     *device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_geneve);
+
+    device_class->connection_type_supported        = NM_SETTING_GENEVE_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_GENEVE_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_GENEVE);
+
+    device_class->link_changed                = link_changed;
+    device_class->unrealize_notify            = unrealize_notify;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->update_connection           = update_connection;
+
+    obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_GENEVE_ID,
+                                                "",
+                                                "",
+                                                0,
+                                                G_MAXUINT32,
+                                                0,
+                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_GENEVE_REMOTE,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_GENEVE_TOS,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_GENEVE_TTL,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DF] = g_param_spec_uint(NM_DEVICE_GENEVE_DF,
+                                                "",
+                                                "",
+                                                0,
+                                                2,
+                                                0,
+                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_GENEVE_DST_PORT,
+                                                      "",
+                                                      "",
+                                                      0,
+                                                      65535,
+                                                      0,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_GENEVE_DEVICE_FACTORY (nm_geneve_device_factory_get_type())
+#define NM_GENEVE_DEVICE_FACTORY(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_GENEVE_DEVICE_FACTORY, NMGeneveDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory      *factory,
+              const char           *iface,
+              const NMPlatformLink *plink,
+              NMConnection         *connection,
+              gboolean             *out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_GENEVE,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Geneve",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_GENEVE,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_GENEVE,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    GENEVE,
+    Geneve,
+    geneve,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_GENEVE)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GENEVE_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-geneve.h b/src/core/devices/nm-device-geneve.h
new file mode 100644
index 00000000..d0a44e9d
--- /dev/null
+++ b/src/core/devices/nm-device-geneve.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_GENEVE_H__
+#define __NETWORKMANAGER_DEVICE_GENEVE_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_GENEVE (nm_device_geneve_get_type())
+#define NM_DEVICE_GENEVE(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneve))
+#define NM_DEVICE_GENEVE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass))
+#define NM_IS_DEVICE_GENEVE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENEVE))
+#define NM_IS_DEVICE_GENEVE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENEVE))
+#define NM_DEVICE_GENEVE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass))
+
+#define NM_DEVICE_GENEVE_ID       "id"
+#define NM_DEVICE_GENEVE_REMOTE   "remote"
+#define NM_DEVICE_GENEVE_TOS      "tos"
+#define NM_DEVICE_GENEVE_TTL      "ttl"
+#define NM_DEVICE_GENEVE_DF       "df"
+#define NM_DEVICE_GENEVE_DST_PORT "dst-port"
+
+typedef struct _NMDeviceGeneve      NMDeviceGeneve;
+typedef struct _NMDeviceGeneveClass NMDeviceGeneveClass;
+
+GType nm_device_geneve_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_GENEVE_H__ */
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 46fb2339..a0cf1ee6 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -5900,7 +5900,6 @@ nm_device_get_route_metric_default(NMDeviceType device_type)
      * in some aspects a VPN. */
     case NM_DEVICE_TYPE_WIREGUARD:
         return NM_VPN_ROUTE_METRIC_DEFAULT;
-
     case NM_DEVICE_TYPE_ETHERNET:
     case NM_DEVICE_TYPE_VETH:
         return 100;
@@ -5934,6 +5933,8 @@ nm_device_get_route_metric_default(NMDeviceType device_type)
         return 470;
     case NM_DEVICE_TYPE_VXLAN:
         return 500;
+    case NM_DEVICE_TYPE_GENEVE:
+        return 525;
     case NM_DEVICE_TYPE_DUMMY:
         return 550;
     case NM_DEVICE_TYPE_WIFI:
@@ -14841,6 +14842,241 @@ impl_device_get_applied_connection(NMDBusObject                      *obj,
 
 /*****************************************************************************/
 
+typedef struct {
+    NMDeviceManaged      managed_state;
+    NMDeviceManagedFlags managed_flags;
+} SetManagedData;
+
+static gboolean
+get_managed_match_by_mac(NMDevice *self, NMDeviceManagedFlags flags, gboolean *out, GError **error)
+{
+    gboolean is_fake_hwaddr;
+
+    nm_assert(out);
+
+    if ((flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC)
+        && (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME)) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                            "cannot match both by 'mac' and by 'interface-name'");
+        return FALSE;
+    }
+
+    nm_device_get_permanent_hw_address_full(self, TRUE, &is_fake_hwaddr);
+
+    if ((flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC) && is_fake_hwaddr) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INVALID_ARGUMENT,
+            "cannot match by 'mac': the device doesn't have a permanent MAC address");
+        return FALSE;
+    }
+
+    if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC)
+        *out = TRUE;
+    else if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME)
+        *out = FALSE;
+    else
+        *out = !is_fake_hwaddr;
+
+    return TRUE;
+}
+
+/**
+ * set_managed:
+ * @self: the device
+ * @managed: the new managed state to set.
+ * @flags: flags to select different behaviors like storing to disk.
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets the managed state of the device. It can affect the runtime managed state
+ * if the %NM_DEVICE_MANAGED_FLAGS_RUNTIME is set, and to the value stored on disk
+ * (persistent across reboots) state if the %NM_DEVICE_MANAGED_FLAGS_PERMANENT is set.
+ *
+ * Returns: %TRUE if the managed state was set successfully, %FALSE otherwise.
+ */
+static gboolean
+set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, GError **error)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(
+        NM_IN_SET(managed, NM_DEVICE_MANAGED_NO, NM_DEVICE_MANAGED_YES, NM_DEVICE_MANAGED_RESET));
+    nm_assert((flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) == 0);
+
+    if (!NM_FLAGS_ANY(flags, NM_DEVICE_MANAGED_FLAGS_PERMANENT | NM_DEVICE_MANAGED_FLAGS_RUNTIME)) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                            _("set managed: no permanent or runtime was selected"));
+        return FALSE;
+    }
+
+    if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT) {
+        NMTernary managed_to_disk, old = NM_TERNARY_DEFAULT;
+        gboolean  by_mac;
+
+        managed_to_disk = managed == NM_DEVICE_MANAGED_RESET ? NM_TERNARY_DEFAULT : !!managed;
+        nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, NULL, error);
+        if (!get_managed_match_by_mac(self, flags, &by_mac, error))
+            return FALSE;
+
+        if (!nm_config_set_device_managed(nm_manager_get_config(priv->manager),
+                                          self,
+                                          managed_to_disk,
+                                          by_mac,
+                                          error))
+            return FALSE;
+
+        /* Update the unmanaged flags after the change on disk */
+        nm_device_set_unmanaged_by_user_conf(self);
+
+        if (managed_to_disk != NM_TERNARY_DEFAULT
+            && managed_to_disk != !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_USER_CONF)) {
+            /* We failed to make the new state effective on disk. Maybe the new config
+             * collides with other config. Try to revert and return error. Otherwise,
+             * we would set the runtime state correctly, but get an unexpected state
+             * after a reboot. */
+            nm_config_set_device_managed(nm_manager_get_config(priv->manager),
+                                         self,
+                                         old,
+                                         by_mac,
+                                         NULL);
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        _("failed to persist 'managed=%d' on disk, other configurations may be "
+                          "overriding it"),
+                        managed);
+            return FALSE;
+        }
+    }
+
+    if (flags & NM_DEVICE_MANAGED_FLAGS_RUNTIME) {
+        if (managed == NM_DEVICE_MANAGED_RESET) {
+            nm_device_set_unmanaged_by_flags(self,
+                                             NM_UNMANAGED_USER_EXPLICIT,
+                                             NM_UNMAN_FLAG_OP_FORGET,
+                                             NM_DEVICE_STATE_REASON_UNMANAGED_USER_EXPLICIT);
+        } else {
+            g_object_set(self, NM_DEVICE_MANAGED, !!managed, NULL);
+
+            /* If requested, set the administrative state of the device to UP if the
+             * new managed state is YES, and to DOWN if it's NO. */
+            if (flags & NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE) {
+                if (nm_device_get_ifindex(self))
+                    nm_platform_link_change_flags(nm_device_get_platform(self),
+                                                  nm_device_get_ifindex(self),
+                                                  IFF_UP,
+                                                  !!managed);
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static void
+set_managed_cb(NMDevice              *self,
+               GDBusMethodInvocation *context,
+               NMAuthSubject         *subject,
+               GError                *error,
+               gpointer               user_data)
+{
+    SetManagedData      *set_managed_data = user_data;
+    NMDeviceManaged      managed;
+    NMDeviceManagedFlags flags;
+    GError              *local = NULL;
+
+    managed = set_managed_data->managed_state;
+    flags   = set_managed_data->managed_flags;
+    nm_g_slice_free(set_managed_data);
+
+    if (!error) {
+        if (!NM_IN_SET(managed,
+                       NM_DEVICE_MANAGED_NO,
+                       NM_DEVICE_MANAGED_YES,
+                       NM_DEVICE_MANAGED_RESET))
+            g_set_error_literal(&error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                                "Invalid managed value");
+        else if ((flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) != 0)
+            g_set_error_literal(&error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                                "Invalid flags");
+    }
+
+    if (error) {
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_MANAGED,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               error->message);
+        g_dbus_method_invocation_return_gerror(context, error);
+        return;
+    }
+
+    if (!set_managed(self, managed, flags, &local)) {
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_MANAGED,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               local->message);
+        g_dbus_method_invocation_take_error(context, g_steal_pointer(&local));
+        return;
+    }
+
+    nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_MANAGED, self, TRUE, NULL, subject, NULL);
+    g_dbus_method_invocation_return_value(context, NULL);
+}
+
+static void
+impl_device_set_managed(NMDBusObject                      *obj,
+                        const NMDBusInterfaceInfoExtended *interface_info,
+                        const NMDBusMethodInfoExtended    *method_info,
+                        GDBusConnection                   *connection,
+                        const char                        *sender,
+                        GDBusMethodInvocation             *invocation,
+                        GVariant                          *parameters)
+{
+    NMDevice             *self  = NM_DEVICE(obj);
+    gs_free_error GError *error = NULL;
+    guint32               managed_u;
+    NMDeviceManaged       managed;
+    guint32               flags_u;
+    NMDeviceManagedFlags  flags;
+    SetManagedData       *set_managed_data;
+
+    g_variant_get(parameters, "(uu)", &managed_u, &flags_u);
+
+    managed = managed_u;
+    flags   = flags_u;
+    nm_assert(managed == managed_u && flags == flags_u);
+
+    set_managed_data  = g_slice_new(SetManagedData);
+    *set_managed_data = (SetManagedData) {
+        .managed_state = managed,
+        .managed_flags = flags,
+    };
+
+    nm_device_auth_request(self,
+                           invocation,
+                           nm_device_get_applied_connection(self),
+                           NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                           TRUE,
+                           NULL,
+                           set_managed_cb,
+                           set_managed_data);
+}
+
+/*****************************************************************************/
+
 static void
 disconnect_cb(NMDevice              *self,
               GDBusMethodInvocation *context,
@@ -19535,7 +19771,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN);
         priv->type = g_value_get_uint(value);
         nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN);
-        nm_assert(priv->type <= NM_DEVICE_TYPE_IPVLAN);
+        nm_assert(priv->type <= NM_DEVICE_TYPE_GENEVE);
         break;
     case PROP_LINK_TYPE:
         /* construct-only */
@@ -19864,6 +20100,12 @@ static const NMDBusInterfaceInfoExtended interface_info_device = {
                         NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"),
                         NM_DEFINE_GDBUS_ARG_INFO("version_id", "t"), ), ),
                 .handle = impl_device_get_applied_connection, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT("SetManaged",
+                                                 .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                                     NM_DEFINE_GDBUS_ARG_INFO("managed", "u"),
+                                                     NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ), ),
+                .handle = impl_device_set_managed, ),
             NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Disconnect", ),
                                                 .handle = impl_device_disconnect, ),
             NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Delete", ),
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 949d8720..8a9bd58c 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -32,11 +32,11 @@ ip4_process_dhcpcd_rfc3442_routes(const char     *iface,
                                   in_addr_t       address,
                                   guint32        *out_gwaddr)
 {
-    gs_free const char **routes = NULL;
-    const char         **r;
-    gboolean             have_routes = FALSE;
+    gs_free char **routes = NULL;
+    char         **r;
+    gboolean       have_routes = FALSE;
 
-    routes = nm_strsplit_set(str, " ");
+    routes = (char **) nm_strsplit_set(str, " ");
     if (!routes)
         return FALSE;
 
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index c746e714..ec33c464 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -1510,8 +1510,8 @@ _domain_track_is_shadowed(GHashTable  *ht,
                           const char **out_parent,
                           int         *out_parent_priority)
 {
-    char *parent;
-    int   parent_priority;
+    const char *parent;
+    int         parent_priority;
 
     if (!ht)
         return FALSE;
diff --git a/src/core/meson.build b/src/core/meson.build
index 6cf891ee..b1d7f2e7 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -102,6 +102,7 @@ libNetworkManager = static_library(
     'devices/nm-device-ethernet-utils.c',
     'devices/nm-device-factory.c',
     'devices/nm-device-generic.c',
+    'devices/nm-device-geneve.c',
     'devices/nm-device-hsr.c',
     'devices/nm-device-infiniband.c',
     'devices/nm-device-ip-tunnel.c',
diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c
index ffcf6e3a..10db0dcb 100644
--- a/src/core/nm-checkpoint.c
+++ b/src/core/nm-checkpoint.c
@@ -39,7 +39,9 @@ typedef struct {
     bool               activation_lifetime_bound_to_profile_visibility : 1;
     bool               settings_connection_is_unsaved : 1;
     bool               settings_connection_is_shadowed_owned : 1;
+    bool               permanent_managed_by_mac : 1;
     NMUnmanFlagOp      unmanaged_explicit;
+    NMTernary          permanent_managed;
     NMActivationReason activation_reason;
     gulong             dev_exported_change_id;
 } DeviceCheckpoint;
@@ -160,7 +162,7 @@ parse_connection_from_shadowed_file(const char *path, GError **error)
 {
     nm_auto_unref_keyfile GKeyFile *keyfile  = NULL;
     gs_free char                   *base_dir = NULL;
-    char                           *sep;
+    const char                     *sep;
 
     keyfile = g_key_file_new();
     if (!g_key_file_load_from_file(keyfile, path, G_KEY_FILE_NONE, error))
@@ -496,14 +498,19 @@ nm_checkpoint_rollback(NMCheckpoint *self)
     /* Start rolling-back each device */
     g_hash_table_iter_init(&iter, priv->devices);
     while (g_hash_table_iter_next(&iter, (gpointer *) &device, (gpointer *) &dev_checkpoint)) {
-        guint32 result = NM_ROLLBACK_RESULT_OK;
+        guint32   result              = NM_ROLLBACK_RESULT_OK;
+        NMTernary perm_managed        = NM_TERNARY_DEFAULT;
+        gboolean  perm_managed_by_mac = FALSE;
+        gboolean  force_perm_managed;
 
         _LOGD("rollback: restoring device %s (state %d, realized %d, explicitly unmanaged %d, "
-              "connection-unsaved %d, connection-shadowed %d, connection-shadowed-owned %d)",
+              "permanently managed %d, connection-unsaved %d, connection-shadowed %d, "
+              "connection-shadowed-owned %d)",
               dev_checkpoint->original_dev_name,
               (int) dev_checkpoint->state,
               dev_checkpoint->realized,
               dev_checkpoint->unmanaged_explicit,
+              dev_checkpoint->permanent_managed,
               dev_checkpoint->settings_connection_is_unsaved,
               !!dev_checkpoint->settings_connection_shadowed,
               dev_checkpoint->settings_connection_is_shadowed_owned);
@@ -541,6 +548,43 @@ nm_checkpoint_rollback(NMCheckpoint *self)
                                                    NM_DEVICE_STATE_REASON_NOW_MANAGED);
         }
 
+        force_perm_managed = !nm_config_get_device_managed(nm_config_get(),
+                                                           device,
+                                                           &perm_managed,
+                                                           &perm_managed_by_mac,
+                                                           NULL);
+
+        if (force_perm_managed || (perm_managed != dev_checkpoint->permanent_managed)
+            || (dev_checkpoint->permanent_managed != NM_TERNARY_DEFAULT
+                && perm_managed_by_mac != dev_checkpoint->permanent_managed_by_mac)) {
+            gs_free_error GError *error = NULL;
+            NMUnmanFlagOp         set_op;
+
+            _LOGD("rollback: restore permanent managed state");
+
+            if (!nm_config_set_device_managed(nm_config_get(),
+                                              device,
+                                              dev_checkpoint->permanent_managed,
+                                              dev_checkpoint->permanent_managed_by_mac,
+                                              &error)) {
+                _LOGE("rollback: failed to restore permanent managed state: %s", error->message);
+                result = NM_ROLLBACK_RESULT_ERR_FAILED;
+                /* even if this failed, we try to continue the rollback */
+            }
+
+            if (dev_checkpoint->permanent_managed == NM_TERNARY_TRUE)
+                set_op = NM_UNMAN_FLAG_OP_SET_MANAGED;
+            else if (dev_checkpoint->permanent_managed == NM_TERNARY_FALSE)
+                set_op = NM_UNMAN_FLAG_OP_SET_UNMANAGED;
+            else
+                set_op = NM_UNMAN_FLAG_OP_FORGET;
+
+            nm_device_set_unmanaged_by_flags_queue(device,
+                                                   NM_UNMANAGED_USER_CONF,
+                                                   set_op,
+                                                   NM_DEVICE_STATE_REASON_NOW_MANAGED);
+        }
+
         if (dev_checkpoint->state == NM_DEVICE_STATE_UNMANAGED) {
             if (nm_device_get_state(device) != NM_DEVICE_STATE_UNMANAGED
                 || dev_checkpoint->unmanaged_explicit == NM_UNMAN_FLAG_OP_SET_UNMANAGED) {
@@ -703,6 +747,8 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device)
     NMSettingsConnection *settings_connection;
     const char           *path;
     NMActRequest         *act_request;
+    gboolean              perm_managed_by_mac;
+    gs_free_error GError *error = NULL;
 
     nm_assert(NM_IS_DEVICE(device));
     nm_assert(nm_device_is_real(device));
@@ -728,12 +774,26 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device)
     } else
         dev_checkpoint->unmanaged_explicit = NM_UNMAN_FLAG_OP_FORGET;
 
+    if (nm_config_get_device_managed(nm_config_get(),
+                                     device,
+                                     &dev_checkpoint->permanent_managed,
+                                     &perm_managed_by_mac,
+                                     NULL)) {
+        dev_checkpoint->permanent_managed_by_mac = perm_managed_by_mac;
+    } else {
+        dev_checkpoint->permanent_managed        = NM_TERNARY_DEFAULT;
+        dev_checkpoint->permanent_managed_by_mac = FALSE;
+        _LOGW("error getting permanent managed state for %s: %s",
+              nm_device_get_iface(device),
+              error->message);
+        g_clear_error(&error);
+    }
+
     act_request = nm_device_get_act_request(device);
     if (act_request) {
-        NMSettingsStorage    *storage;
-        gboolean              shadowed_owned = FALSE;
-        const char           *shadowed_file;
-        gs_free_error GError *error = NULL;
+        NMSettingsStorage *storage;
+        gboolean           shadowed_owned = FALSE;
+        const char        *shadowed_file;
 
         settings_connection = nm_act_request_get_settings_connection(act_request);
         applied_connection  = nm_act_request_get_applied_connection(act_request);
@@ -764,6 +824,7 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device)
                 _LOGW("error reading shadowed connection file for %s: %s",
                       nm_device_get_iface(device),
                       error->message);
+                g_clear_error(&error);
             }
         }
     }
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index 461fd8ed..dd0822ad 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -2058,12 +2058,15 @@ _match_section_infos_construct(GKeyFile *keyfile, gboolean is_device)
 {
     char            **groups;
     gsize             i, j, ngroups;
-    char             *connection_tag      = NULL;
+    char             *main_group          = NULL;
     MatchSectionInfo *match_section_infos = NULL;
-    const char       *prefix;
+    const char       *prefix, *prefix_intern;
 
     prefix =
         is_device ? NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE : NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION;
+    prefix_intern =
+        is_device ? NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE
+                  : NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN NM_CONFIG_KEYFILE_GROUPPREFIX_CONNECTION;
 
     /* get the list of existing [connection.\+]/[device.\+] sections.
      *
@@ -2074,27 +2077,36 @@ _match_section_infos_construct(GKeyFile *keyfile, gboolean is_device)
     if (!groups)
         return NULL;
 
-    if (ngroups > 0) {
-        gsize l = strlen(prefix);
-
-        for (i = 0, j = 0; i < ngroups; i++) {
-            if (g_str_has_prefix(groups[i], prefix)) {
-                if (groups[i][l] == '\0')
-                    connection_tag = groups[i];
-                else
-                    groups[j++] = groups[i];
-            } else
-                g_free(groups[i]);
+    for (i = 0, j = 0; i < ngroups; i++) {
+        if (nm_streq0(groups[i], prefix)) {
+            main_group = groups[i];
+        } else if (nm_streq0(groups[i], prefix_intern)) {
+            /* [.intern.connection] and [.intern.device] should not exist */
+            _nm_log(LOGL_WARN,
+                    LOGD_CORE,
+                    0,
+                    NULL,
+                    NULL,
+                    "Invalid [.intern.*] section 'connection' or 'device' found");
+            g_free(groups[i]);
+            continue;
+        } else if (g_str_has_prefix(groups[i], prefix)) {
+            groups[j++] = groups[i];
+        } else if (g_str_has_prefix(groups[i], prefix_intern)) {
+            /* [.intern.connection-whatever] and [.intern.device-whatever] can exist */
+            groups[j++] = groups[i];
+        } else {
+            g_free(groups[i]);
         }
-        ngroups = j;
     }
+    ngroups = j;
 
-    if (ngroups == 0 && !connection_tag) {
+    if (ngroups == 0 && !main_group) {
         g_free(groups);
         return NULL;
     }
 
-    match_section_infos = g_new0(MatchSectionInfo, ngroups + 1 + (connection_tag ? 1 : 0));
+    match_section_infos            = g_new0(MatchSectionInfo, ngroups + 1 + (main_group ? 1 : 0));
     match_section_infos->is_device = is_device;
     for (i = 0; i < ngroups; i++) {
         /* pass ownership of @group on... */
@@ -2103,9 +2115,9 @@ _match_section_infos_construct(GKeyFile *keyfile, gboolean is_device)
                                  groups[ngroups - i - 1],
                                  is_device);
     }
-    if (connection_tag) {
-        /* pass ownership of @connection_tag on... */
-        _match_section_info_init(&match_section_infos[i], keyfile, connection_tag, is_device);
+    if (main_group) {
+        /* pass ownership of @main_group on... */
+        _match_section_info_init(&match_section_infos[i], keyfile, main_group, is_device);
     }
     g_free(groups);
 
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index d1f2bbed..d8bf2e3e 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -2075,6 +2075,210 @@ nm_config_set_connectivity_check_enabled(NMConfig *self, gboolean enabled)
     g_key_file_unref(keyfile);
 }
 
+/*****************************************************************************/
+
+static gboolean
+normalize_hwaddr_for_group_name(const char *hwaddr, char *out, GError **error)
+{
+    guint8 hwaddr_bin[NM_UTILS_HWADDR_LEN_MAX];
+    gsize  hwaddr_bin_len;
+
+    if (!_nm_utils_hwaddr_aton(hwaddr, hwaddr_bin, sizeof(hwaddr_bin), &hwaddr_bin_len)) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                    "Invalid MAC address: %s",
+                    hwaddr);
+        return FALSE;
+    }
+    nm_utils_bin2hexstr_full(hwaddr_bin, hwaddr_bin_len, '-', TRUE, out);
+    return TRUE;
+}
+
+/**
+ * nm_config_get_device_managed:
+ * @self: the NMConfig instance
+ * @device: the interface
+ * @out: (out): the managed state of the device
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: TRUE if there were no errors, FALSE otherwise.
+ */
+gboolean
+nm_config_get_device_managed(NMConfig  *self,
+                             NMDevice  *device,
+                             NMTernary *out_managed,
+                             gboolean  *out_by_mac,
+                             GError   **error)
+{
+    NMConfigPrivate *priv;
+    const GKeyFile  *keyfile       = NULL;
+    gs_free char    *group_by_name = NULL;
+    gs_free char    *group_by_mac  = NULL;
+    const char      *ifname        = nm_device_get_iface(device);
+    const char      *hwaddr        = nm_device_get_permanent_hw_address(device);
+    char             mac_group_name[NM_UTILS_HWADDR_LEN_MAX * 3 + 1];
+    NMTernary        val_by_name, val_by_mac = NM_TERNARY_DEFAULT;
+
+    g_return_val_if_fail(NM_IS_CONFIG(self), FALSE);
+    g_return_val_if_fail(NM_CONFIG_GET_PRIVATE(self)->config_data, FALSE);
+    g_return_val_if_fail(out_managed, FALSE);
+    g_return_val_if_fail(ifname, FALSE);
+
+    priv    = NM_CONFIG_GET_PRIVATE(self);
+    keyfile = _nm_config_data_get_keyfile_intern(priv->config_data);
+
+    if (!keyfile) {
+        NM_SET_OUT(out_managed, NM_TERNARY_DEFAULT);
+        NM_SET_OUT(out_by_mac, FALSE);
+        return TRUE;
+    }
+
+    group_by_name =
+        g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s", ifname);
+
+    val_by_name = (NMTernary) nm_config_keyfile_get_boolean(keyfile,
+                                                            group_by_name,
+                                                            NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                            NM_TERNARY_DEFAULT);
+
+    /* Devices without a kernel link (i.e. OVS ports) don't have a MAC address */
+    if (hwaddr) {
+        if (!normalize_hwaddr_for_group_name(hwaddr, mac_group_name, error))
+            return FALSE;
+
+        group_by_mac = g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s",
+                                       mac_group_name);
+
+        val_by_mac = (NMTernary) nm_config_keyfile_get_boolean(keyfile,
+                                                               group_by_mac,
+                                                               NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                               NM_TERNARY_DEFAULT);
+    }
+
+    if (val_by_name != NM_TERNARY_DEFAULT && val_by_mac == NM_TERNARY_DEFAULT) {
+        NM_SET_OUT(out_managed, val_by_name);
+        NM_SET_OUT(out_by_mac, FALSE);
+        return TRUE;
+    } else if (val_by_mac != NM_TERNARY_DEFAULT && val_by_name == NM_TERNARY_DEFAULT) {
+        NM_SET_OUT(out_managed, val_by_mac);
+        NM_SET_OUT(out_by_mac, TRUE);
+        return TRUE;
+    } else if (val_by_name == NM_TERNARY_DEFAULT && val_by_mac == NM_TERNARY_DEFAULT) {
+        NM_SET_OUT(out_managed, NM_TERNARY_DEFAULT);
+        NM_SET_OUT(out_by_mac, FALSE);
+        return TRUE;
+    } else if (val_by_name == val_by_mac) {
+        NM_SET_OUT(out_managed, val_by_name);
+        NM_SET_OUT(out_by_mac, FALSE);
+        return TRUE;
+    }
+
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_FAILED,
+                "Multiple managed states found for device: %s",
+                nm_device_get_iface(device));
+    return FALSE;
+}
+
+/**
+ * nm_config_set_device_managed:
+ * @self: the NMConfig instance
+ * @device: the NMDevice instance associated with this config change
+ * @managed: the managed state to set
+ * @by_mac: if %TRUE, match by MAC address, otherwise by interface name. This is
+ *   only used when @managed = TRUE.
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets the managed state of the device to the intern keyfile. Here we store the
+ * configuration received via the D-Bus API. Configurations from other config
+ * files are still in place and may have higher precedence.
+ *
+ * Prior to setting the new state, the existing configuration is removed. If
+ * @managed is set to %NM_TERNARY_DEFAULT, we only do the removal of the previous
+ * configuration.
+ */
+gboolean
+nm_config_set_device_managed(NMConfig *self,
+                             NMDevice *device,
+                             NMTernary managed,
+                             gboolean  by_mac,
+                             GError  **error)
+{
+    NMConfigPrivate *priv;
+    g_autoptr(GKeyFile) keyfile = NULL;
+    char         *group;
+    gs_free char *group_by_name = NULL;
+    gs_free char *group_by_mac  = NULL;
+    gs_free char *match_value   = NULL;
+    gboolean      changed       = FALSE;
+    const char   *ifname        = nm_device_get_iface(device);
+    const char   *hwaddr        = nm_device_get_permanent_hw_address(device);
+    char          mac_group_name[NM_UTILS_HWADDR_LEN_MAX * 3 + 1];
+
+    g_return_val_if_fail(NM_IS_CONFIG(self), FALSE);
+    g_return_val_if_fail(NM_CONFIG_GET_PRIVATE(self)->config_data, FALSE);
+    g_return_val_if_fail(ifname, FALSE);
+
+    if (by_mac && !hwaddr) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                    "the device has no MAC address, but match by MAC was requested");
+        return FALSE;
+    }
+
+    if (hwaddr && !normalize_hwaddr_for_group_name(hwaddr, mac_group_name, error))
+        return FALSE;
+
+    priv    = NM_CONFIG_GET_PRIVATE(self);
+    keyfile = nm_config_data_clone_keyfile_intern(priv->config_data);
+
+    /* Remove existing configs. Search them by group name [.intern.device-manage-*]. In
+     * the intern file, 'device-manage' sections are only used for this purpose, so we
+     * won't remove any other device's config. */
+    group_by_name =
+        g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s", ifname);
+
+    if (g_key_file_remove_group(keyfile, group_by_name, NULL))
+        changed = TRUE;
+
+    /* Devices without a kernel link (i.e. OVS ports) don't have a MAC address */
+    if (hwaddr) {
+        group_by_mac = g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s",
+                                       mac_group_name);
+
+        if (g_key_file_remove_group(keyfile, group_by_mac, NULL))
+            changed = TRUE;
+    }
+
+    /* If the new state is not explicitly TRUE of FALSE, we only remove the configs */
+    if (managed == NM_TERNARY_DEFAULT)
+        goto done;
+
+    /* Set new values */
+    if (by_mac) {
+        group       = group_by_mac;
+        match_value = g_strdup_printf("mac:%s", hwaddr);
+    } else {
+        group       = group_by_name;
+        match_value = g_strdup_printf("interface-name:=%s", ifname);
+    }
+
+    g_key_file_set_value(keyfile, group, NM_CONFIG_KEYFILE_KEY_MATCH_DEVICE, match_value);
+    g_key_file_set_value(keyfile, group, NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED, managed ? "1" : "0");
+    changed = TRUE;
+
+done:
+    if (changed)
+        nm_config_set_values(self, keyfile, TRUE, FALSE);
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
 /**
  * nm_config_set_values:
  * @self: the NMConfig instance
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index 5518184c..0cfbdfe4 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -142,6 +142,17 @@ gboolean nm_config_set_global_dns(NMConfig *self, NMGlobalDnsConfig *global_dns,
 
 void nm_config_set_connectivity_check_enabled(NMConfig *self, gboolean enabled);
 
+gboolean nm_config_get_device_managed(NMConfig  *self,
+                                      NMDevice  *device,
+                                      NMTernary *out_managed,
+                                      gboolean  *out_by_mac,
+                                      GError   **error);
+gboolean nm_config_set_device_managed(NMConfig *self,
+                                      NMDevice *device,
+                                      NMTernary managed,
+                                      gboolean  by_mac,
+                                      GError  **error);
+
 /* internal defines ... */
 extern guint _nm_config_match_nm_version;
 extern char *_nm_config_match_env;
diff --git a/src/core/nm-ip-config.c b/src/core/nm-ip-config.c
index 975ae20d..75a75b42 100644
--- a/src/core/nm-ip-config.c
+++ b/src/core/nm-ip-config.c
@@ -54,7 +54,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);
 
 /*****************************************************************************/
@@ -75,7 +75,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);
 
@@ -96,7 +96,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);
@@ -844,7 +844,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 47a40bd2..02d654c4 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-l3-config-data.c b/src/core/nm-l3-config-data.c
index 328f59b6..7500337e 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -2991,12 +2991,9 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
              * the one we create here (because the "onlink" flag is part of the
              * identifier of a route, see nm_platform_ip4_route_cmp()).
              *
-             * Note however that for ECMP routes we currently can only merge routes
-             * that agree in their onlink flag. So a route without gateway cannot
-             * merge with an onlink route that has a gateway. That needs fixing,
-             * by not treating the onlink flag as for the entire route, but allowing
-             * to merge ECMP routes with different onlink flag. And first, we need
-             * to track the onlink flag for the nexthop (NMPlatformIP4RtNextHop). */
+             * The onlink flag is tracked per-nexthop (in NMPlatformIP4RtNextHop.rtnh_flags
+             * for extra nexthops, and in r_rtm_flags for the first nexthop). ECMP routes
+             * can be merged regardless of per-nexthop onlink flags. */
             r.r4.r_rtm_flags &= ~((unsigned) RTNH_F_ONLINK);
         }
 
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 0d93f76b..88a9c241 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -624,7 +624,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:
@@ -1571,7 +1571,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 5f0721da..c103c9f2 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/nm-netns.c b/src/core/nm-netns.c
index f55d1132..0e8b15a7 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -207,6 +207,7 @@ _ecmp_track_sort_lst_cmp(const CList *a, const CList *b, const void *user_data)
     NM_CMP_FIELD(route_a, route_b, ifindex);
     NM_CMP_FIELD(route_b, route_a, weight);
     NM_CMP_DIRECT(htonl(route_a->gateway), htonl(route_b->gateway));
+    NM_CMP_DIRECT(route_a->r_rtm_flags & RTNH_F_ONLINK, route_b->r_rtm_flags & RTNH_F_ONLINK);
 
     return nm_assert_unreachable_val(
         nm_platform_ip4_route_cmp(route_a, route_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID));
@@ -275,9 +276,10 @@ _ecmp_track_init_merged_obj(EcmpTrackEcmpid *track_ecmpid, const NMPObject **out
             NMPlatformIP4RtNextHop   *nh = (gpointer) &obj_new->_ip4_route.extra_nexthops[i - 1];
 
             *nh = (NMPlatformIP4RtNextHop) {
-                .ifindex = r->ifindex,
-                .gateway = r->gateway,
-                .weight  = r->weight,
+                .ifindex    = r->ifindex,
+                .gateway    = r->gateway,
+                .weight     = r->weight,
+                .rtnh_flags = r->r_rtm_flags & RTNH_F_ONLINK,
             };
         }
         i++;
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index 91445b30..f1f7caee 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
@@ -2394,6 +2394,61 @@ nmtstp_link_dummy_add(NMPlatform *platform, int external_command, const char *na
 }
 
 const NMPlatformLink *
+nmtstp_link_geneve_add(NMPlatform                *platform,
+                       int                        external_command,
+                       const char                *name,
+                       const NMPlatformLnkGeneve *lnk)
+{
+    const NMPlatformLink *pllink = NULL;
+    int                   success;
+
+    g_assert(nm_utils_ifname_valid_kernel(name, NULL));
+    g_assert(lnk->remote || !IN6_IS_ADDR_UNSPECIFIED(&lnk->remote6));
+
+    external_command = nmtstp_run_command_check_external(external_command);
+
+    _init_platform(&platform, external_command);
+
+    if (external_command) {
+        char remote[NM_INET_ADDRSTRLEN];
+        char remote6[NM_INET_ADDRSTRLEN];
+        char str_ttl[30];
+
+        if (lnk->remote)
+            nm_inet4_ntop(lnk->remote, remote);
+        else
+            remote[0] = '\0';
+
+        if (memcmp(&lnk->remote6, &in6addr_any, sizeof(in6addr_any)))
+            nm_inet6_ntop(&lnk->remote6, remote6);
+        else
+            remote6[0] = '\0';
+
+        success = !nmtstp_run_command(
+            "ip link add %s type geneve id %u remote %s %s tos %02x dstport %u%s",
+            name,
+            lnk->id,
+            remote[0] ? remote : remote6,
+            lnk->ttl > 0    ? nm_sprintf_buf(str_ttl, "ttl %u", lnk->ttl & 0xff)
+            : lnk->ttl == 0 ? "ttl auto"
+                            : "ttl inherit",
+            lnk->tos,
+            lnk->dst_port,
+            lnk->df == 1   ? " df set "
+            : lnk->df == 2 ? " df inherit "
+                           : "");
+
+        if (success)
+            pllink = nmtstp_assert_wait_for_link(platform, name, NM_LINK_TYPE_GENEVE, 100);
+    } else
+        success = NMTST_NM_ERR_SUCCESS(nm_platform_link_geneve_add(platform, name, lnk, &pllink));
+
+    _assert_pllink(platform, success, pllink, name, NM_LINK_TYPE_GENEVE);
+
+    return pllink;
+}
+
+const NMPlatformLink *
 nmtstp_link_gre_add(NMPlatform             *platform,
                     int                     external_command,
                     const char             *name,
@@ -3024,8 +3079,6 @@ nmtstp_link_vxlan_add(NMPlatform               *platform,
     return pllink;
 }
 
-/*****************************************************************************/
-
 const NMPlatformLink *
 nmtstp_link_get_typed(NMPlatform *platform, int ifindex, const char *name, NMLinkType link_type)
 {
diff --git a/src/core/platform/tests/test-common.h b/src/core/platform/tests/test-common.h
index 85ed7961..251fa510 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);
 
 /*****************************************************************************/
 
@@ -474,6 +474,10 @@ const NMPlatformLink *nmtstp_link_veth_add(NMPlatform *platform,
                                            const char *peer);
 const NMPlatformLink *
 nmtstp_link_dummy_add(NMPlatform *platform, int external_command, const char *name);
+const NMPlatformLink *nmtstp_link_geneve_add(NMPlatform                *platform,
+                                             int                        external_command,
+                                             const char                *name,
+                                             const NMPlatformLnkGeneve *lnk);
 const NMPlatformLink *nmtstp_link_gre_add(NMPlatform             *platform,
                                           int                     external_command,
                                           const char             *name,
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index 77e7e641..5618308f 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -1388,10 +1388,11 @@ test_software_detect(gconstpointer user_data)
     const gboolean                   ext        = test_data->external_command;
     NMPlatformLnkBridge              lnk_bridge = {};
     NMPlatformLnkTun                 lnk_tun;
-    NMPlatformLnkGre                 lnk_gre  = {};
-    NMPlatformLnkVti                 lnk_vti  = {};
-    NMPlatformLnkVti6                lnk_vti6 = {};
-    nm_auto_close int                tun_fd   = -1;
+    NMPlatformLnkGeneve              lnk_geneve = {};
+    NMPlatformLnkGre                 lnk_gre    = {};
+    NMPlatformLnkVti                 lnk_vti    = {};
+    NMPlatformLnkVti6                lnk_vti6   = {};
+    nm_auto_close int                tun_fd     = -1;
     gboolean                         module_loaded;
 
     nmtstp_run_command_check("ip link add %s type dummy", PARENT_NAME);
@@ -1434,6 +1435,31 @@ test_software_detect(gconstpointer user_data)
             g_error("Failed adding Bridge interface");
         break;
 
+    case NM_LINK_TYPE_GENEVE:
+    {
+        switch (test_data->test_mode) {
+        case 0:
+            lnk_geneve.id       = 42;
+            lnk_geneve.remote   = nmtst_inet4_from_string("192.168.1.100");
+            lnk_geneve.ttl      = 64;
+            lnk_geneve.tos      = 0;
+            lnk_geneve.dst_port = 6081;
+            lnk_geneve.df       = 0;
+            break;
+        case 1:
+            lnk_geneve.id       = 12345;
+            lnk_geneve.remote6  = nmtst_inet6_from_string("2001:db8::1");
+            lnk_geneve.ttl      = 128;
+            lnk_geneve.tos      = 16;
+            lnk_geneve.dst_port = 6082;
+            lnk_geneve.df       = 1;
+            break;
+        }
+
+        g_assert(nmtstp_link_geneve_add(NULL, ext, DEVICE_NAME, &lnk_geneve));
+        break;
+    }
+
     case NM_LINK_TYPE_GRE:
         module_loaded = nmtstp_ensure_module("ip_gre");
 
@@ -2208,6 +2234,34 @@ test_software_detect(gconstpointer user_data)
             }
             break;
         }
+        case NM_LINK_TYPE_GENEVE:
+        {
+            const NMPlatformLnkGeneve *plnk = &lnk->lnk_geneve;
+
+            g_assert(plnk == nm_platform_link_get_lnk_geneve(NM_PLATFORM_GET, ifindex, NULL));
+
+            switch (test_data->test_mode) {
+            case 0:
+                g_assert_cmpint(plnk->id, ==, 42);
+                nmtst_assert_ip4_address(plnk->remote, "192.168.1.100");
+                nmtst_assert_ip6_address(&plnk->remote6, "::");
+                g_assert_cmpint(plnk->ttl, ==, 64);
+                g_assert_cmpint(plnk->tos, ==, 0);
+                g_assert_cmpint(plnk->dst_port, ==, 6081);
+                g_assert_cmpint(plnk->df, ==, 0);
+                break;
+            case 1:
+                g_assert_cmpint(plnk->id, ==, 12345);
+                nmtst_assert_ip4_address(plnk->remote, "0.0.0.0");
+                nmtst_assert_ip6_address(&plnk->remote6, "2001:db8::1");
+                g_assert_cmpint(plnk->ttl, ==, 128);
+                g_assert_cmpint(plnk->tos, ==, 16);
+                g_assert_cmpint(plnk->dst_port, ==, 6082);
+                g_assert_cmpint(plnk->df, ==, 1);
+                break;
+            }
+            break;
+        }
         case NM_LINK_TYPE_WIREGUARD:
         {
             const NMPlatformLnkWireGuard *plnk = &lnk->lnk_wireguard;
@@ -4143,6 +4197,8 @@ _nmtstp_setup_tests(void)
         g_test_add_func("/link/external", test_external);
 
         test_software_detect_add("/link/software/detect/bridge", NM_LINK_TYPE_BRIDGE, 0);
+        test_software_detect_add("/link/software/detect/geneve/0", NM_LINK_TYPE_GENEVE, 0);
+        test_software_detect_add("/link/software/detect/geneve/1", NM_LINK_TYPE_GENEVE, 1);
         test_software_detect_add("/link/software/detect/gre", NM_LINK_TYPE_GRE, 0);
         test_software_detect_add("/link/software/detect/gretap", NM_LINK_TYPE_GRETAP, 0);
         test_software_detect_add("/link/software/detect/ip6tnl/0", NM_LINK_TYPE_IP6TNL, 0);
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index fbad2447..27bb7431 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -2461,6 +2461,78 @@ done:
 }
 
 /*****************************************************************************/
+static void
+test_ip4_rtnh_onlink(void)
+{
+    /* Extra nexthops that differ only in rtnh_flags ONLINK should
+     * compare as different. */
+    NMPlatformIP4RtNextHop nh_a = {
+        .ifindex    = 2,
+        .gateway    = nmtst_inet4_from_string("10.10.10.10"),
+        .weight     = 1,
+        .rtnh_flags = 0,
+    };
+
+    NMPlatformIP4RtNextHop nh_b = nh_a;
+    nh_b.rtnh_flags             = RTNH_F_ONLINK;
+
+    g_assert_cmpint(nm_platform_ip4_rt_nexthop_cmp(&nh_a, &nh_b, TRUE), !=, 0);
+    g_assert_cmpint(nm_platform_ip4_rt_nexthop_cmp(&nh_a, &nh_b, FALSE), !=, 0);
+
+    nh_b.rtnh_flags = 0;
+    g_assert_cmpint(nm_platform_ip4_rt_nexthop_cmp(&nh_a, &nh_b, TRUE), ==, 0);
+    g_assert_cmpint(nm_platform_ip4_rt_nexthop_cmp(&nh_a, &nh_b, FALSE), ==, 0);
+}
+
+static void
+test_ip4_route_onlink_per_nexthop(void)
+{
+    NMPlatformIP4Route r_a = {};
+    NMPlatformIP4Route r_b;
+
+    /* Two single-hop routes that are identical, except for the onlink flag. */
+    r_a.ifindex      = 1;
+    r_a.rt_source    = NM_IP_CONFIG_SOURCE_USER;
+    r_a.network      = nmtst_inet4_from_string("10.10.10.10");
+    r_a.plen         = 24;
+    r_a.gateway      = nmtst_inet4_from_string("10.10.10.1");
+    r_a.metric       = 100;
+    r_a.n_nexthops   = 1;
+    r_a.type_coerced = nm_platform_route_type_coerce(RTN_UNICAST);
+    r_a.scope_inv    = nm_platform_route_scope_inv(RT_SCOPE_UNIVERSE);
+
+    r_b             = r_a;
+    r_b.r_rtm_flags = RTNH_F_ONLINK;
+
+    /* Onlink flag should result in the same ECMP_ID but different IDs. */
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID),
+                    ==,
+                    0);
+
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID), !=, 0);
+    g_assert_cmpint(
+        nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY),
+        !=,
+        0);
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL),
+                    !=,
+                    0);
+
+    r_b.r_rtm_flags = 0;
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ID), ==, 0);
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_ECMP_ID),
+                    ==,
+                    0);
+    g_assert_cmpint(
+        nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_SEMANTICALLY),
+        ==,
+        0);
+    g_assert_cmpint(nm_platform_ip4_route_cmp(&r_a, &r_b, NM_PLATFORM_IP_ROUTE_CMP_TYPE_FULL),
+                    ==,
+                    0);
+}
+
+/*****************************************************************************/
 
 NMTstpSetupFunc const _nmtstp_setup_platform_func = SETUP;
 
@@ -2480,6 +2552,8 @@ _nmtstp_setup_tests(void)
     nmtstp_env1_add_test_func_data(testpath, test_func, arg, 2, TRUE)
 
     add_test_func("/route/ip4", test_ip4_route);
+    add_test_func("/route/ip4_onlink_per_nexthop", test_ip4_route_onlink_per_nexthop);
+    add_test_func("/route/ip4_rtnh_onlink", test_ip4_rtnh_onlink);
     add_test_func("/route/ip6", test_ip6_route);
     add_test_func("/route/ip4_metric0", test_ip4_route_metric0);
     add_test_func_data("/route/ip4_options/1", test_ip4_route_options, GINT_TO_POINTER(1));
diff --git a/src/core/settings/nm-agent-manager.c b/src/core/settings/nm-agent-manager.c
index 19b3cfcb..ce7dbab2 100644
--- a/src/core/settings/nm-agent-manager.c
+++ b/src/core/settings/nm-agent-manager.c
@@ -1083,6 +1083,39 @@ _con_get_request_start_validated(NMAuthChain           *chain,
     _con_get_request_start_proceed(req, req->con.current_has_modify);
 }
 
+static gboolean
+_req_has_existing_secrets(Request *req)
+{
+    GVariantIter iter;
+    const char  *setting_name;
+    GVariant    *setting_dict;
+    gboolean     has;
+
+    if (!req->con.get.existing_secrets)
+        return FALSE;
+
+    nm_assert(g_variant_is_of_type(req->con.get.existing_secrets, NM_VARIANT_TYPE_CONNECTION));
+
+    g_variant_iter_init(&iter, req->con.get.existing_secrets);
+    while (g_variant_iter_next(&iter, "{&s@a{sv}}", &setting_name, &setting_dict)) {
+        GVariantIter setting_iter;
+        GVariant    *val;
+
+        g_variant_iter_init(&setting_iter, setting_dict);
+        while (g_variant_iter_next(&setting_iter, "{&sv}", NULL, &val)) {
+            has = !g_variant_is_container(val) || g_variant_n_children(val) > 0;
+            g_variant_unref(val);
+            if (has) {
+                g_variant_unref(setting_dict);
+                return TRUE;
+            }
+        }
+        g_variant_unref(setting_dict);
+    }
+
+    return FALSE;
+}
+
 static void
 _con_get_request_start(Request *req)
 {
@@ -1103,7 +1136,7 @@ _con_get_request_start(Request *req)
      * unprivileged users.
      */
     if ((req->con.get.flags != NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE)
-        && (req->con.get.existing_secrets
+        && (_req_has_existing_secrets(req)
             || _nm_connection_aggregate(req->con.connection,
                                         NM_CONNECTION_AGGREGATE_ANY_SYSTEM_SECRET_FLAGS,
                                         NULL))) {
diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c
index d5611e76..7ed3712b 100644
--- a/src/core/settings/nm-settings-connection.c
+++ b/src/core/settings/nm-settings-connection.c
@@ -781,7 +781,8 @@ validate_secret_flags(NMConnection *connection, GVariant *secrets, ForEachSecret
 static gboolean
 secret_is_system_owned(NMSettingSecretFlags flags, gpointer user_data)
 {
-    return !NM_FLAGS_HAS(flags, NM_SETTING_SECRET_FLAG_AGENT_OWNED);
+    return !NM_FLAGS_ANY(flags,
+                         NM_SETTING_SECRET_FLAG_AGENT_OWNED | NM_SETTING_SECRET_FLAG_NOT_SAVED);
 }
 
 static void
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
index b9e3f919..728dccac 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
@@ -77,7 +77,7 @@ get_full_file_path(const char *ifcfg_path, const char *file_path)
 {
     const char   *base    = file_path;
     gs_free char *dirname = NULL;
-    char         *p;
+    const char   *p;
 
     g_return_val_if_fail(ifcfg_path != NULL, NULL);
     g_return_val_if_fail(file_path != NULL, NULL);
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index 6e5e44d2..cca53d82 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -212,18 +212,19 @@ validate_type_utf8(const struct Opt *opt, const char *value, const guint32 len)
 }
 
 static gboolean
-validate_type_keyword(const struct Opt *opt, const char *value, const guint32 len)
+validate_type_keyword(const struct Opt *opt, const char *value_in, const guint32 len)
 {
     gs_free char *value_free = NULL;
+    char         *value;
 
     nm_assert(opt);
-    nm_assert(value);
+    nm_assert(value_in);
 
     /* Allow everything */
     if (!opt->str_allowed)
         return TRUE;
 
-    value = nm_strndup_a(300, value, len, &value_free);
+    value = nm_strndup_a(300, value_in, len, &value_free);
 
     /* validate each space-separated word in 'value' */
 
diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c
index 78fd1057..b2f29821 100644
--- a/src/core/tests/config/test-config.c
+++ b/src/core/tests/config/test-config.c
@@ -312,6 +312,108 @@ test_config_override(void)
 }
 
 static void
+test_config_managed(void)
+{
+    NMConfig     *config;
+    const char   *CONFIG_USER   = BUILD_DIR "/test-config-managed.conf";
+    const char   *CONFIG_INTERN = BUILD_DIR "/test-config-managed-intern.conf";
+    NMDevice     *dev;
+    gs_free char *group_by_name = NULL;
+    const char   *ifname, *group_by_mac;
+    NMTernary     managed;
+    gboolean      by_mac;
+    GKeyFile     *kf = nm_config_create_keyfile();
+
+    dev    = nm_test_device_new("11:11:11:11:11:11");
+    ifname = nm_device_get_iface(dev);
+    group_by_name =
+        g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s", ifname);
+    group_by_mac = NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-11-11-11-11-11-11";
+
+    g_assert(g_file_set_contents(CONFIG_USER, "", 0, NULL));
+    g_assert(g_file_set_contents(CONFIG_INTERN, "", 0, NULL));
+
+    config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL);
+
+    g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT);
+
+    /* Matching by name */
+    NMTST_EXPECT_NM_INFO("config: signal: *");
+    g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, FALSE, NULL));
+    g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_TRUE);
+    g_assert_false(by_mac);
+    g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL);
+    g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL));
+    g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL));
+    g_assert_cmpint(g_key_file_get_integer(kf, group_by_name, "managed", NULL), ==, 1);
+
+    NMTST_EXPECT_NM_INFO("config: signal: *");
+    g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, FALSE, NULL));
+    g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_FALSE);
+    g_assert_false(by_mac);
+    g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL);
+    g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL));
+    g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL));
+    g_assert_cmpint(g_key_file_get_integer(kf, group_by_name, "managed", NULL), ==, 0);
+
+    /* Matching by MAC address */
+    NMTST_EXPECT_NM_INFO("config: signal: *");
+    g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, TRUE, NULL));
+    g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_TRUE);
+    g_assert_true(by_mac);
+    g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL);
+    g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL));
+    g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL));
+    g_assert_cmpint(g_key_file_get_integer(kf, group_by_mac, "managed", NULL), ==, 1);
+
+    NMTST_EXPECT_NM_INFO("config: signal: *");
+    g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, TRUE, NULL));
+    g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_FALSE);
+    g_assert_true(by_mac);
+    g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL);
+    g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL));
+    g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL));
+    g_assert_cmpint(g_key_file_get_integer(kf, group_by_mac, "managed", NULL), ==, 0);
+
+    /* Resetting the managed state */
+    NMTST_EXPECT_NM_INFO("config: signal: *");
+    g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_DEFAULT, FALSE, NULL));
+    g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT);
+    g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL);
+    g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL));
+    g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL));
+
+    g_object_unref(config);
+
+    /* Both values set in the intern config file, different values */
+    g_key_file_set_string(kf, group_by_name, "managed", "1");
+    g_key_file_set_string(kf, group_by_mac, "managed", "0");
+    g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL));
+    config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL);
+    g_assert(!nm_config_get_device_managed(config, dev, &managed, NULL, NULL));
+
+    g_object_unref(config);
+
+    /* Both values set in the intern config file, same values */
+    g_key_file_set_string(kf, group_by_name, "managed", "1");
+    g_key_file_set_string(kf, group_by_mac, "managed", "1");
+    g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL));
+    config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL);
+    g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL));
+    g_assert_cmpint(managed, ==, NM_TERNARY_TRUE);
+
+    g_key_file_unref(kf);
+    g_object_unref(dev);
+    g_object_unref(config);
+}
+
+static void
 test_config_global_dns(void)
 {
     NMConfig                *config;
@@ -1412,6 +1514,7 @@ main(int argc, char **argv)
 
     g_test_add_func("/config/set-values", test_config_set_values);
     g_test_add_func("/config/global-dns", test_config_global_dns);
+    g_test_add_func("/config/managed", test_config_managed);
     g_test_add_func("/config/connectivity-check", test_config_connectivity_check);
 
     g_test_add_func("/config/signal", test_config_signal);
diff --git a/src/libnm-base/nm-config-base.h b/src/libnm-base/nm-config-base.h
index d101c95c..e11d8abf 100644
--- a/src/libnm-base/nm-config-base.h
+++ b/src/libnm-base/nm-config-base.h
@@ -89,4 +89,7 @@
 #define NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_GLOBAL_DNS_DOMAIN \
     NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN NM_CONFIG_KEYFILE_GROUPPREFIX_GLOBAL_DNS_DOMAIN
 
+#define NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE \
+    NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN NM_CONFIG_KEYFILE_GROUPPREFIX_DEVICE
+
 #endif /* __NM_CONFIG_BASE_H__ */
diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver
index 6c48978a..c03b8f39 100644
--- a/src/libnm-client-impl/libnm.ver
+++ b/src/libnm-client-impl/libnm.ver
@@ -2103,3 +2103,28 @@ global:
 	nm_utils_copy_cert_as_user;
 	nm_vpn_plugin_info_supports_safe_private_file_access;
 } libnm_1_54_0;
+
+libnm_1_56_1 {
+global:
+	nm_connection_get_setting_geneve;
+	nm_device_geneve_get_df;
+	nm_device_geneve_get_dst_port;
+	nm_device_geneve_get_id;
+	nm_device_geneve_get_remote;
+	nm_device_geneve_get_tos;
+	nm_device_geneve_get_ttl;
+	nm_device_geneve_get_type;
+	nm_device_managed_flags_get_type;
+	nm_device_managed_get_type;
+	nm_device_set_managed_async;
+	nm_device_set_managed_finish;
+	nm_setting_geneve_df_get_type;
+	nm_setting_geneve_get_destination_port;
+	nm_setting_geneve_get_df;
+	nm_setting_geneve_get_id;
+	nm_setting_geneve_get_remote;
+	nm_setting_geneve_get_tos;
+	nm_setting_geneve_get_ttl;
+	nm_setting_geneve_get_type;
+	nm_setting_geneve_new;
+} libnm_1_56_0;
diff --git a/src/libnm-client-impl/meson.build b/src/libnm-client-impl/meson.build
index b4936629..3352ebfe 100644
--- a/src/libnm-client-impl/meson.build
+++ b/src/libnm-client-impl/meson.build
@@ -16,6 +16,7 @@ libnm_client_impl_sources = files(
   'nm-device-bt.c',
   'nm-device-dummy.c',
   'nm-device-ethernet.c',
+  'nm-device-geneve.c',
   'nm-device-generic.c',
   'nm-device-hsr.c',
   'nm-device-infiniband.c',
diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c
index b81ac6e5..f1383526 100644
--- a/src/libnm-client-impl/nm-client.c
+++ b/src/libnm-client-impl/nm-client.c
@@ -29,6 +29,7 @@
 #include "nm-device-dummy.h"
 #include "nm-device-ethernet.h"
 #include "nm-device-generic.h"
+#include "nm-device-geneve.h"
 #include "nm-device-hsr.h"
 #include "nm-device-infiniband.h"
 #include "nm-device-ip-tunnel.h"
diff --git a/src/libnm-client-impl/nm-device-geneve.c b/src/libnm-client-impl/nm-device-geneve.c
new file mode 100644
index 00000000..312a99b3
--- /dev/null
+++ b/src/libnm-client-impl/nm-device-geneve.c
@@ -0,0 +1,344 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#include "libnm-client-impl/nm-default-libnm.h"
+
+#include "nm-device-geneve.h"
+
+#include "nm-setting-connection.h"
+#include "nm-setting-geneve.h"
+#include "nm-utils.h"
+#include "nm-object-private.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_ID,
+                                  PROP_REMOTE,
+                                  PROP_TOS,
+                                  PROP_TTL,
+                                  PROP_DST_PORT,
+                                  PROP_DF, );
+
+typedef struct {
+    char   *remote;
+    guint32 id;
+    gint32  ttl;
+    guint16 dst_port;
+    guint8  df;
+    guint8  tos;
+} NMDeviceGenevePrivate;
+
+struct _NMDeviceGeneve {
+    NMDevice              parent;
+    NMDeviceGenevePrivate _priv;
+};
+
+struct _NMDeviceGeneveClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceGeneve, nm_device_geneve, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_GENEVE_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceGeneve, NM_IS_DEVICE_GENEVE, NMObject, NMDevice)
+
+/*****************************************************************************/
+
+/**
+ * nm_device_geneve_get_id:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the device's GENEVE ID.
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_device_geneve_get_id(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0);
+
+    return NM_DEVICE_GENEVE_GET_PRIVATE(device)->id;
+}
+
+/**
+ * nm_device_geneve_get_remote:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the IP address of the remote tunnel endpoint
+ *
+ * Since: 1.58, 1.56.1
+ **/
+const char *
+nm_device_geneve_get_remote(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), NULL);
+
+    return _nml_coerce_property_str_not_empty(NM_DEVICE_GENEVE_GET_PRIVATE(device)->remote);
+}
+
+/**
+ * nm_device_geneve_get_dst_port:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the UDP destination port
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_device_geneve_get_dst_port(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0);
+
+    return NM_DEVICE_GENEVE_GET_PRIVATE(device)->dst_port;
+}
+
+/**
+ * nm_device_geneve_get_tos:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the TOS value to use in outgoing packets
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_device_geneve_get_tos(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0);
+
+    return NM_DEVICE_GENEVE_GET_PRIVATE(device)->tos;
+}
+
+/**
+ * nm_device_geneve_get_ttl:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the time-to-live value to use in outgoing packets
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_device_geneve_get_ttl(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0);
+
+    return NM_DEVICE_GENEVE_GET_PRIVATE(device)->ttl;
+}
+
+/**
+ * nm_device_geneve_get_df:
+ * @device: a #NMDeviceGeneve
+ *
+ * Returns: the Don't Fragment (DF) bit to set in outgoing packets
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_device_geneve_get_df(NMDeviceGeneve *device)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_GENEVE(device), 0);
+
+    return NM_DEVICE_GENEVE_GET_PRIVATE(device)->df;
+}
+
+static gboolean
+connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingGeneve *s_geneve;
+
+    if (!NM_DEVICE_CLASS(nm_device_geneve_parent_class)
+             ->connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (!nm_connection_is_type(connection, NM_SETTING_GENEVE_SETTING_NAME)) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                            _("The connection was not a GENEVE connection."));
+        return FALSE;
+    }
+
+    s_geneve = nm_connection_get_setting_geneve(connection);
+    if (nm_setting_geneve_get_id(s_geneve) != nm_device_geneve_get_id(NM_DEVICE_GENEVE(device))) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+            _("The GENEVE identifiers of the device and the connection didn't match."));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static GType
+get_setting_type(NMDevice *device)
+{
+    return NM_TYPE_SETTING_GENEVE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceGeneve *device = NM_DEVICE_GENEVE(object);
+
+    switch (prop_id) {
+    case PROP_ID:
+        g_value_set_uint(value, nm_device_geneve_get_id(device));
+        break;
+    case PROP_REMOTE:
+        g_value_set_string(value, nm_device_geneve_get_remote(device));
+        break;
+    case PROP_TOS:
+        g_value_set_uint(value, nm_device_geneve_get_tos(device));
+        break;
+    case PROP_TTL:
+        g_value_set_int(value, nm_device_geneve_get_ttl(device));
+        break;
+    case PROP_DST_PORT:
+        g_value_set_uint(value, nm_device_geneve_get_dst_port(device));
+        break;
+    case PROP_DF:
+        g_value_set_uint(value, nm_device_geneve_get_df(device));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+nm_device_geneve_init(NMDeviceGeneve *device)
+{}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceGenevePrivate *priv = NM_DEVICE_GENEVE_GET_PRIVATE(object);
+
+    g_free(priv->remote);
+
+    G_OBJECT_CLASS(nm_device_geneve_parent_class)->finalize(object);
+}
+
+const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_geneve = NML_DBUS_META_IFACE_INIT_PROP(
+    NM_DBUS_INTERFACE_DEVICE_GENEVE,
+    nm_device_geneve_get_type,
+    NML_DBUS_META_INTERFACE_PRIO_INSTANTIATE_30,
+    NML_DBUS_META_IFACE_DBUS_PROPERTIES(
+        NML_DBUS_META_PROPERTY_INIT_Y("Df", PROP_DF, NMDeviceGeneve, _priv.df),
+        NML_DBUS_META_PROPERTY_INIT_Q("DstPort", PROP_DST_PORT, NMDeviceGeneve, _priv.dst_port),
+        NML_DBUS_META_PROPERTY_INIT_U("Id", PROP_ID, NMDeviceGeneve, _priv.id),
+        NML_DBUS_META_PROPERTY_INIT_S("Remote", PROP_REMOTE, NMDeviceGeneve, _priv.remote),
+        NML_DBUS_META_PROPERTY_INIT_Y("Tos", PROP_TOS, NMDeviceGeneve, _priv.tos),
+        NML_DBUS_META_PROPERTY_INIT_I("Ttl", PROP_TTL, NMDeviceGeneve, _priv.ttl), ), );
+
+static void
+nm_device_geneve_class_init(NMDeviceGeneveClass *klass)
+{
+    GObjectClass  *object_class    = G_OBJECT_CLASS(klass);
+    NMObjectClass *nm_object_class = NM_OBJECT_CLASS(klass);
+    NMDeviceClass *device_class    = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+    object_class->finalize     = finalize;
+
+    _NM_OBJECT_CLASS_INIT_PRIV_PTR_DIRECT(nm_object_class, NMDeviceGeneve);
+
+    device_class->connection_compatible = connection_compatible;
+    device_class->get_setting_type      = get_setting_type;
+
+    /**
+     * NMDeviceGeneve:id:
+     *
+     * The device's GENEVE ID.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_GENEVE_ID,
+                                                "",
+                                                "",
+                                                0,
+                                                (1 << 24) - 1,
+                                                0,
+                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    /**
+     * NMDeviceGeneve:remote:
+     *
+     * The IP address of the remote tunnel endpoint.
+     *
+     * Since: 1.58, 1.56.1
+     */
+    obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_GENEVE_REMOTE,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    /**
+     * NMDeviceGeneve:tos:
+     *
+     * The TOS value to use in outgoing packets.
+     *
+     * Since: 1.58, 1.56.1
+     */
+    obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_GENEVE_TOS,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    /**
+     * NMDeviceGeneve:ttl:
+     *
+     * The time-to-live value to use in outgoing packets.
+     *
+     * Since: 1.58, 1.56.1
+     */
+    obj_properties[PROP_TTL] = g_param_spec_int(NM_DEVICE_GENEVE_TTL,
+                                                "",
+                                                "",
+                                                -1,
+                                                255,
+                                                0,
+                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    /**
+     * NMDeviceGeneve:dst-port:
+     *
+     * The UDP destination port used to communicate with the remote GENEVE tunnel
+     * endpoint.
+     *
+     * Since: 1.58, 1.56.1
+     */
+    obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_GENEVE_DST_PORT,
+                                                      "",
+                                                      "",
+                                                      0,
+                                                      65535,
+                                                      0,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    /**
+     * NMDeviceGeneve:df:
+     *
+     * The Don't Fragment (DF) bit to set in outgoing packets.
+     *
+     * Since: 1.58, 1.56.1
+     */
+    obj_properties[PROP_DF] = g_param_spec_uchar(NM_DEVICE_GENEVE_DF,
+                                                 "",
+                                                 "",
+                                                 0,
+                                                 2,
+                                                 0,
+                                                 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    _nml_dbus_meta_class_init_with_properties(object_class, &_nml_dbus_meta_iface_nm_device_geneve);
+}
diff --git a/src/libnm-client-impl/nm-device.c b/src/libnm-client-impl/nm-device.c
index 1712efa5..63a62942 100644
--- a/src/libnm-client-impl/nm-device.c
+++ b/src/libnm-client-impl/nm-device.c
@@ -302,6 +302,7 @@ coerce_type(NMDeviceType type)
     case NM_DEVICE_TYPE_TUN:
     case NM_DEVICE_TYPE_VETH:
     case NM_DEVICE_TYPE_GENERIC:
+    case NM_DEVICE_TYPE_GENEVE:
     case NM_DEVICE_TYPE_UNUSED1:
     case NM_DEVICE_TYPE_UNUSED2:
     case NM_DEVICE_TYPE_UNKNOWN:
@@ -1445,9 +1446,7 @@ nm_device_get_managed(NMDevice *device)
  *
  * Since: 1.2
  *
- * Deprecated: 1.22: Use the async command nm_client_dbus_set_property() on
- * nm_object_get_path(), interface %NM_DBUS_INTERFACE_DEVICE to set the
- * "Managed" property to a "(b)" boolean value.
+ * Deprecated: 1.22: Use nm_device_set_managed_async() instead.
  * This function is deprecated because it calls a synchronous D-Bus method
  * and modifies the content of the NMClient cache client side. Also, it does
  * not emit a property changed signal.
@@ -1470,6 +1469,72 @@ nm_device_set_managed(NMDevice *device, gboolean managed)
 }
 
 /**
+ * nm_device_set_managed_async:
+ * @device: a #NMDevice
+ * @managed: the managed state
+ * @flags: the flags argument. See #NMDeviceManagedFlags.
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: callback to be called when the set_managed operation completes
+ * @user_data: caller-specific data passed to @callback
+ *
+ * Asynchronously begins setting the managed state of the device. With the flags
+ * argument different behaviors can be achieved, such as persisting the state to
+ * disk or matching the device by MAC address.
+ *
+ * Since: 1.58, 1.56.1
+ **/
+void
+nm_device_set_managed_async(NMDevice            *device,
+                            NMDeviceManaged      managed,
+                            NMDeviceManagedFlags flags,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+    g_return_if_fail(NM_IS_DEVICE(device));
+    g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable));
+
+    _nm_client_dbus_call(_nm_object_get_client(device),
+                         device,
+                         nm_device_set_managed_async,
+                         cancellable,
+                         callback,
+                         user_data,
+                         _nm_object_get_path(device),
+                         NM_DBUS_INTERFACE_DEVICE,
+                         "SetManaged",
+                         g_variant_new("(uu)", managed, flags),
+                         G_VARIANT_TYPE("()"),
+                         G_DBUS_CALL_FLAGS_NONE,
+                         NM_DBUS_DEFAULT_TIMEOUT_MSEC,
+                         nm_dbus_connection_call_finish_void_strip_dbus_error_cb);
+}
+
+/**
+ * nm_device_set_managed_finish:
+ * @device: a #NMDevice
+ * @result: the result passed to the #GAsyncReadyCallback
+ * @error: location for a #GError, or %NULL
+ *
+ * Gets the result of a call to nm_device_set_managed_async().
+ *
+ * Returns: %TRUE on success, %FALSE on error, in which case @error
+ * will be set.
+ *
+ * Since: 1.58, 1.56.1
+ **/
+gboolean
+nm_device_set_managed_finish(NMDevice *device, GAsyncResult *result, GError **error)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+    g_return_val_if_fail(nm_g_task_is_valid(result, device, nm_device_set_managed_async), FALSE);
+
+    return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+/*****************************************************************************/
+
+/**
  * nm_device_get_autoconnect:
  * @device: a #NMDevice
  *
@@ -1792,6 +1857,8 @@ get_type_name(NMDevice *device)
         return _("MACVLAN");
     case NM_DEVICE_TYPE_VXLAN:
         return _("VXLAN");
+    case NM_DEVICE_TYPE_GENEVE:
+        return _("GENEVE");
     case NM_DEVICE_TYPE_IP_TUNNEL:
         return _("IPTunnel");
     case NM_DEVICE_TYPE_TUN:
diff --git a/src/libnm-client-impl/nm-libnm-utils.c b/src/libnm-client-impl/nm-libnm-utils.c
index 1b25c92c..1c80c1dc 100644
--- a/src/libnm-client-impl/nm-libnm-utils.c
+++ b/src/libnm-client-impl/nm-libnm-utils.c
@@ -782,6 +782,7 @@ const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[] = {
     &_nml_dbus_meta_iface_nm_device_bridge,
     &_nml_dbus_meta_iface_nm_device_dummy,
     &_nml_dbus_meta_iface_nm_device_generic,
+    &_nml_dbus_meta_iface_nm_device_geneve,
     &_nml_dbus_meta_iface_nm_device_hsr,
     &_nml_dbus_meta_iface_nm_device_iptunnel,
     &_nml_dbus_meta_iface_nm_device_infiniband,
diff --git a/src/libnm-client-impl/nm-libnm-utils.h b/src/libnm-client-impl/nm-libnm-utils.h
index 7dcf8c18..27b77b60 100644
--- a/src/libnm-client-impl/nm-libnm-utils.h
+++ b/src/libnm-client-impl/nm-libnm-utils.h
@@ -579,7 +579,7 @@ struct _NMLDBusMetaIface {
                              NML_DBUS_META_IFACE_OBJ_PROPERTIES(),                              \
                              ##__VA_ARGS__)
 
-extern const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[47];
+extern const NMLDBusMetaIface *const _nml_dbus_meta_ifaces[48];
 
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_accesspoint;
@@ -593,6 +593,7 @@ extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_bond;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_bridge;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_dummy;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_generic;
+extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_geneve;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_hsr;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_infiniband;
 extern const NMLDBusMetaIface _nml_dbus_meta_iface_nm_device_iptunnel;
diff --git a/src/libnm-client-public/NetworkManager.h b/src/libnm-client-public/NetworkManager.h
index 9e9a4eb8..880bac6e 100644
--- a/src/libnm-client-public/NetworkManager.h
+++ b/src/libnm-client-public/NetworkManager.h
@@ -37,6 +37,7 @@
 #include "nm-setting-dummy.h"
 #include "nm-setting-ethtool.h"
 #include "nm-setting-generic.h"
+#include "nm-setting-geneve.h"
 #include "nm-setting-gsm.h"
 #include "nm-setting-hostname.h"
 #include "nm-setting-hsr.h"
@@ -114,6 +115,7 @@
 #include "nm-device-dummy.h"
 #include "nm-device-ethernet.h"
 #include "nm-device-generic.h"
+#include "nm-device-geneve.h"
 #include "nm-device-hsr.h"
 #include "nm-device-infiniband.h"
 #include "nm-device-ip-tunnel.h"
diff --git a/src/libnm-client-public/meson.build b/src/libnm-client-public/meson.build
index b8ae9cce..5aa6de25 100644
--- a/src/libnm-client-public/meson.build
+++ b/src/libnm-client-public/meson.build
@@ -18,6 +18,7 @@ libnm_client_headers = files(
   'nm-device-dummy.h',
   'nm-device-ethernet.h',
   'nm-device-generic.h',
+  'nm-device-geneve.h',
   'nm-device-hsr.h',
   'nm-device-infiniband.h',
   'nm-device-ip-tunnel.h',
diff --git a/src/libnm-client-public/nm-autoptr.h b/src/libnm-client-public/nm-autoptr.h
index f4321ad3..12379fe1 100644
--- a/src/libnm-client-public/nm-autoptr.h
+++ b/src/libnm-client-public/nm-autoptr.h
@@ -41,6 +41,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceBt, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceDummy, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceEthernet, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceGeneric, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceGeneve, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceHsr, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceIPTunnel, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMDeviceInfiniband, g_object_unref)
@@ -80,6 +81,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingDcb, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingDummy, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingEthtool, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingGeneric, g_object_unref)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingGeneve, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingGsm, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingHostname, g_object_unref)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(NMSettingHsr, g_object_unref)
diff --git a/src/libnm-client-public/nm-device-geneve.h b/src/libnm-client-public/nm-device-geneve.h
new file mode 100644
index 00000000..1c537fbd
--- /dev/null
+++ b/src/libnm-client-public/nm-device-geneve.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEVICE_GENEVE_H__
+#define __NM_DEVICE_GENEVE_H__
+
+#if !defined(__NETWORKMANAGER_H_INSIDE__) && !defined(NETWORKMANAGER_COMPILATION)
+#error "Only <NetworkManager.h> can be included directly."
+#endif
+
+#include "nm-device.h"
+
+G_BEGIN_DECLS
+
+#define NM_TYPE_DEVICE_GENEVE (nm_device_geneve_get_type())
+#define NM_DEVICE_GENEVE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneve))
+#define NM_DEVICE_GENEVE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass))
+#define NM_IS_DEVICE_GENEVE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENEVE))
+#define NM_IS_DEVICE_GENEVE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENEVE))
+#define NM_DEVICE_GENEVE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENEVE, NMDeviceGeneveClass))
+
+#define NM_DEVICE_GENEVE_ID       "id"
+#define NM_DEVICE_GENEVE_REMOTE   "remote"
+#define NM_DEVICE_GENEVE_TOS      "tos"
+#define NM_DEVICE_GENEVE_TTL      "ttl"
+#define NM_DEVICE_GENEVE_DST_PORT "dst-port"
+#define NM_DEVICE_GENEVE_DF       "df"
+
+/**
+ * NMDeviceGeneve:
+ *
+ * Since: 1.58, 1.56.1
+ */
+typedef struct _NMDeviceGeneve      NMDeviceGeneve;
+typedef struct _NMDeviceGeneveClass NMDeviceGeneveClass;
+
+NM_AVAILABLE_IN_1_56_1
+GType nm_device_geneve_get_type(void);
+
+NM_AVAILABLE_IN_1_56_1
+guint nm_device_geneve_get_id(NMDeviceGeneve *device);
+NM_AVAILABLE_IN_1_56_1
+const char *nm_device_geneve_get_remote(NMDeviceGeneve *device);
+NM_AVAILABLE_IN_1_56_1
+guint nm_device_geneve_get_dst_port(NMDeviceGeneve *device);
+NM_AVAILABLE_IN_1_56_1
+guint nm_device_geneve_get_tos(NMDeviceGeneve *device);
+NM_AVAILABLE_IN_1_56_1
+guint nm_device_geneve_get_ttl(NMDeviceGeneve *device);
+NM_AVAILABLE_IN_1_56_1
+guint nm_device_geneve_get_df(NMDeviceGeneve *device);
+
+G_END_DECLS
+
+#endif /* __NM_DEVICE_GENEVE_H__ */
diff --git a/src/libnm-client-public/nm-device.h b/src/libnm-client-public/nm-device.h
index f58d3ddb..2ee3a4ae 100644
--- a/src/libnm-client-public/nm-device.h
+++ b/src/libnm-client-public/nm-device.h
@@ -150,6 +150,15 @@ NM_AVAILABLE_IN_1_2
 NM_DEPRECATED_IN_1_22
 _NM_DEPRECATED_SYNC_METHOD
 void nm_device_set_managed(NMDevice *device, gboolean managed);
+NM_AVAILABLE_IN_1_56_1
+void nm_device_set_managed_async(NMDevice            *device,
+                                 NMDeviceManaged      managed,
+                                 NMDeviceManagedFlags flags,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data);
+NM_AVAILABLE_IN_1_56_1
+gboolean nm_device_set_managed_finish(NMDevice *device, GAsyncResult *result, GError **error);
 
 gboolean nm_device_get_autoconnect(NMDevice *device);
 
diff --git a/src/libnm-core-aux-intern/nm-libnm-core-utils.c b/src/libnm-core-aux-intern/nm-libnm-core-utils.c
index 2d709e20..a39f6454 100644
--- a/src/libnm-core-aux-intern/nm-libnm-core-utils.c
+++ b/src/libnm-core-aux-intern/nm-libnm-core-utils.c
@@ -483,8 +483,8 @@ nm_utils_validate_shared_dhcp_range(const char *shared_dhcp_range,
                                     GPtrArray  *addresses,
                                     GError    **error)
 {
-    char         *start_address_str;
-    char         *end_address_str;
+    const char   *start_address_str;
+    const char   *end_address_str;
     NMIPAddress  *interface_address_with_prefix;
     NMIPAddr      interface_address;
     NMIPAddr      start_address;
@@ -825,7 +825,7 @@ nm_dns_uri_parse(int addr_family, const char *str, NMDnsServer *dns, GError **er
         addr = nm_strndup_a(100, addr_port, end - addr_port, &addr_heap);
 
         /* IPv6 link-local scope-id */
-        perc = strchr(addr, '%');
+        perc = (char *) strchr(addr, '%');
         if (perc) {
             *perc = '\0';
             if (g_strlcpy(dns->interface, perc + 1, sizeof(dns->interface))
diff --git a/src/libnm-core-impl/gen-metadata-nm-settings-libnm-core.xml.in b/src/libnm-core-impl/gen-metadata-nm-settings-libnm-core.xml.in
index c83deea9..8d5d3bc9 100644
--- a/src/libnm-core-impl/gen-metadata-nm-settings-libnm-core.xml.in
+++ b/src/libnm-core-impl/gen-metadata-nm-settings-libnm-core.xml.in
@@ -1378,6 +1378,34 @@
                   gprop-type="gchararray"
                   />
     </setting>
+    <setting name="geneve"
+             gtype="NMSettingGeneve"
+             >
+        <property name="destination-port"
+                  dbus-type="u"
+                  gprop-type="guint"
+                  />
+        <property name="df"
+                  dbus-type="i"
+                  gprop-type="gint"
+                  />
+        <property name="id"
+                  dbus-type="u"
+                  gprop-type="guint"
+                  />
+        <property name="remote"
+                  dbus-type="s"
+                  gprop-type="gchararray"
+                  />
+        <property name="tos"
+                  dbus-type="u"
+                  gprop-type="guint"
+                  />
+        <property name="ttl"
+                  dbus-type="i"
+                  gprop-type="gint"
+                  />
+    </setting>
     <setting name="gsm"
              gtype="NMSettingGsm"
              >
diff --git a/src/libnm-core-impl/meson.build b/src/libnm-core-impl/meson.build
index 610e2279..1eee3184 100644
--- a/src/libnm-core-impl/meson.build
+++ b/src/libnm-core-impl/meson.build
@@ -17,6 +17,7 @@ libnm_core_settings_sources = files(
   'nm-setting-dummy.c',
   'nm-setting-ethtool.c',
   'nm-setting-generic.c',
+  'nm-setting-geneve.c',
   'nm-setting-gsm.c',
   'nm-setting-hostname.c',
   'nm-setting-hsr.c',
diff --git a/src/libnm-core-impl/nm-connection.c b/src/libnm-core-impl/nm-connection.c
index 0fbadfb8..180fcbbc 100644
--- a/src/libnm-core-impl/nm-connection.c
+++ b/src/libnm-core-impl/nm-connection.c
@@ -3272,6 +3272,7 @@ nm_connection_is_virtual(NMConnection *connection)
                      NM_SETTING_BOND_SETTING_NAME,
                      NM_SETTING_BRIDGE_SETTING_NAME,
                      NM_SETTING_DUMMY_SETTING_NAME,
+                     NM_SETTING_GENEVE_SETTING_NAME,
                      NM_SETTING_HSR_SETTING_NAME,
                      NM_SETTING_IP_TUNNEL_SETTING_NAME,
                      NM_SETTING_IPVLAN_SETTING_NAME,
@@ -3527,6 +3528,22 @@ nm_connection_get_setting_generic(NMConnection *connection)
 }
 
 /**
+ * nm_connection_get_setting_geneve:
+ * @connection: the #NMConnection
+ *
+ * A shortcut to return any #NMSettingGeneve the connection might contain.
+ *
+ * Returns: (transfer none): an #NMSettingGeneve if the connection contains one, otherwise NULL
+ *
+ * Since: 1.58, 1.56.1
+ **/
+NMSettingGeneve *
+nm_connection_get_setting_geneve(NMConnection *connection)
+{
+    return _nm_connection_get_setting_by_metatype(connection, NM_META_SETTING_TYPE_GENEVE);
+}
+
+/**
  * nm_connection_get_setting_gsm:
  * @connection: the #NMConnection
  *
diff --git a/src/libnm-core-impl/nm-meta-setting-base-impl.c b/src/libnm-core-impl/nm-meta-setting-base-impl.c
index 52af9ad2..d84490d2 100644
--- a/src/libnm-core-impl/nm-meta-setting-base-impl.c
+++ b/src/libnm-core-impl/nm-meta-setting-base-impl.c
@@ -27,6 +27,7 @@
 #include "nm-setting-dummy.h"
 #include "nm-setting-ethtool.h"
 #include "nm-setting-generic.h"
+#include "nm-setting-geneve.h"
 #include "nm-setting-gsm.h"
 #include "nm-setting-hostname.h"
 #include "nm-setting-hsr.h"
@@ -324,6 +325,13 @@ const NMMetaSettingInfo nm_meta_setting_infos[] = {
             .setting_name      = NM_SETTING_GENERIC_SETTING_NAME,
             .get_setting_gtype = nm_setting_generic_get_type,
         },
+    [NM_META_SETTING_TYPE_GENEVE] =
+        {
+            .meta_type         = NM_META_SETTING_TYPE_GENEVE,
+            .setting_priority  = NM_SETTING_PRIORITY_HW_BASE,
+            .setting_name      = NM_SETTING_GENEVE_SETTING_NAME,
+            .get_setting_gtype = nm_setting_geneve_get_type,
+        },
     [NM_META_SETTING_TYPE_GSM] =
         {
             .meta_type         = NM_META_SETTING_TYPE_GSM,
@@ -655,6 +663,7 @@ const NMMetaSettingType nm_meta_setting_types_by_priority[] = {
     NM_META_SETTING_TYPE_CDMA,
     NM_META_SETTING_TYPE_DUMMY,
     NM_META_SETTING_TYPE_GENERIC,
+    NM_META_SETTING_TYPE_GENEVE,
     NM_META_SETTING_TYPE_GSM,
     NM_META_SETTING_TYPE_HSR,
     NM_META_SETTING_TYPE_INFINIBAND,
diff --git a/src/libnm-core-impl/nm-setting-bridge.c b/src/libnm-core-impl/nm-setting-bridge.c
index c1618434..21457412 100644
--- a/src/libnm-core-impl/nm-setting-bridge.c
+++ b/src/libnm-core-impl/nm-setting-bridge.c
@@ -472,17 +472,17 @@ nm_bridge_vlan_to_str(const NMBridgeVlan *vlan, GError **error)
 NMBridgeVlan *
 nm_bridge_vlan_from_str(const char *str, GError **error)
 {
-    NMBridgeVlan        *vlan   = NULL;
-    gs_free const char **tokens = NULL;
-    guint                i, vid_start, vid_end = 0;
-    gboolean             pvid     = FALSE;
-    gboolean             untagged = FALSE;
-    char                *c;
+    NMBridgeVlan  *vlan   = NULL;
+    gs_free char **tokens = NULL;
+    guint          i, vid_start, vid_end = 0;
+    gboolean       pvid     = FALSE;
+    gboolean       untagged = FALSE;
+    char          *c;
 
     g_return_val_if_fail(str, NULL);
     g_return_val_if_fail(!error || !*error, NULL);
 
-    tokens = nm_utils_escaped_tokens_split(str, NM_ASCII_SPACES);
+    tokens = (char **) nm_utils_escaped_tokens_split(str, NM_ASCII_SPACES);
     if (!tokens || !tokens[0]) {
         g_set_error_literal(error,
                             NM_CONNECTION_ERROR,
diff --git a/src/libnm-core-impl/nm-setting-geneve.c b/src/libnm-core-impl/nm-setting-geneve.c
new file mode 100644
index 00000000..b01fcd9a
--- /dev/null
+++ b/src/libnm-core-impl/nm-setting-geneve.c
@@ -0,0 +1,354 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#include "libnm-core-impl/nm-default-libnm-core.h"
+
+#include "nm-setting-geneve.h"
+
+#include <stdlib.h>
+
+#include "nm-utils.h"
+#include "nm-setting-private.h"
+
+/**
+ * SECTION:nm-setting-geneve
+ * @short_description: Describes connection properties for GENEVE interfaces
+ *
+ * The #NMSettingGeneve object is a #NMSetting subclass that describes properties
+ * necessary for connection to GENEVE interfaces.
+ **/
+
+#define DST_PORT_DEFAULT 6081
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_ID,
+                                  PROP_REMOTE,
+                                  PROP_DESTINATION_PORT,
+                                  PROP_TOS,
+                                  PROP_TTL,
+                                  PROP_DF, );
+
+typedef struct {
+    char   *remote;
+    guint32 id;
+    guint32 destination_port;
+    guint32 tos;
+    gint32  ttl;
+    int     df;
+} NMSettingGenevePrivate;
+
+/**
+ * NMSettingGeneve:
+ *
+ * GENEVE Settings
+ */
+struct _NMSettingGeneve {
+    NMSetting              parent;
+    NMSettingGenevePrivate _priv;
+};
+
+struct _NMSettingGeneveClass {
+    NMSettingClass parent;
+};
+
+G_DEFINE_TYPE(NMSettingGeneve, nm_setting_geneve, NM_TYPE_SETTING)
+
+#define NM_SETTING_GENEVE_GET_PRIVATE(o) \
+    _NM_GET_PRIVATE(o, NMSettingGeneve, NM_IS_SETTING_GENEVE, NMSetting)
+
+/*****************************************************************************/
+
+/**
+ * nm_setting_geneve_get_id:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:id property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_setting_geneve_get_id(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), 0);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->id;
+}
+
+/**
+ * nm_setting_geneve_get_remote:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:remote property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+const char *
+nm_setting_geneve_get_remote(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), NULL);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->remote;
+}
+
+/**
+ * nm_setting_geneve_get_destination_port:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:destination-port property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_setting_geneve_get_destination_port(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), DST_PORT_DEFAULT);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->destination_port;
+}
+
+/**
+ * nm_setting_geneve_get_tos:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:tos property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_setting_geneve_get_tos(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), 0);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->tos;
+}
+
+/**
+ * nm_setting_geneve_get_ttl:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:ttl property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+guint
+nm_setting_geneve_get_ttl(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), 0);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->ttl;
+}
+
+/**
+ * nm_setting_geneve_get_df:
+ * @setting: the #NMSettingGeneve
+ *
+ * Returns: the #NMSettingGeneve:df property of the setting
+ *
+ * Since: 1.58, 1.56.1
+ **/
+NMSettingGeneveDf
+nm_setting_geneve_get_df(NMSettingGeneve *setting)
+{
+    g_return_val_if_fail(NM_IS_SETTING_GENEVE(setting), NM_SETTING_GENEVE_DF_UNSET);
+
+    return NM_SETTING_GENEVE_GET_PRIVATE(setting)->df;
+}
+
+/*****************************************************************************/
+
+static gboolean
+verify(NMSetting *setting, NMConnection *connection, GError **error)
+{
+    NMSettingGenevePrivate *priv = NM_SETTING_GENEVE_GET_PRIVATE(setting);
+
+    if (priv->id == 0) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_MISSING_PROPERTY,
+                    _("property is required"));
+        g_prefix_error(error, "%s.%s: ", NM_SETTING_GENEVE_SETTING_NAME, NM_SETTING_GENEVE_ID);
+        return FALSE;
+    }
+
+    if (!priv->remote) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_MISSING_PROPERTY,
+                    _("property is required"));
+        g_prefix_error(error, "%s.%s: ", NM_SETTING_GENEVE_SETTING_NAME, NM_SETTING_GENEVE_REMOTE);
+        return FALSE;
+    }
+
+    if (!nm_inet_parse_bin(AF_UNSPEC, priv->remote, NULL, NULL)) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("'%s' is not a valid IP address"),
+                    priv->remote);
+        g_prefix_error(error, "%s.%s: ", NM_SETTING_GENEVE_SETTING_NAME, NM_SETTING_GENEVE_REMOTE);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_setting_geneve_init(NMSettingGeneve *self)
+{}
+
+/**
+ * nm_setting_geneve_new:
+ *
+ * Creates a new #NMSettingGeneve object with default values.
+ *
+ * Returns: (transfer full): the new empty #NMSettingGeneve object
+ *
+ * Since: 1.58, 1.56.1
+ **/
+NMSetting *
+nm_setting_geneve_new(void)
+{
+    return g_object_new(NM_TYPE_SETTING_GENEVE, NULL);
+}
+
+static void
+nm_setting_geneve_class_init(NMSettingGeneveClass *klass)
+{
+    GObjectClass   *object_class        = G_OBJECT_CLASS(klass);
+    NMSettingClass *setting_class       = NM_SETTING_CLASS(klass);
+    GArray         *properties_override = _nm_sett_info_property_override_create_array();
+
+    object_class->get_property = _nm_setting_property_get_property_direct;
+    object_class->set_property = _nm_setting_property_set_property_direct;
+
+    setting_class->verify = verify;
+
+    /**
+     * NMSettingGeneve:id:
+     *
+     * Specifies the GENEVE Network Identifier (or GENEVE Segment Identifier) to
+     * use.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_uint32(properties_override,
+                                              obj_properties,
+                                              NM_SETTING_GENEVE_ID,
+                                              PROP_ID,
+                                              0,
+                                              (1 << 24) - 1,
+                                              0,
+                                              NM_SETTING_PARAM_INFERRABLE,
+                                              NMSettingGenevePrivate,
+                                              id);
+
+    /**
+     * NMSettingGeneve:remote:
+     *
+     * Specifies the unicast destination IP address to use in outgoing packets
+     * when communicating with the remote GENEVE tunnel endpoint.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_string(properties_override,
+                                              obj_properties,
+                                              NM_SETTING_GENEVE_REMOTE,
+                                              PROP_REMOTE,
+                                              NM_SETTING_PARAM_REQUIRED,
+                                              NMSettingGenevePrivate,
+                                              remote,
+                                              .direct_set_string_ip_address_addr_family =
+                                                  AF_UNSPEC + 1,
+                                              .direct_string_allow_empty = TRUE);
+
+    /**
+     * NMSettingGeneve:destination-port:
+     *
+     * Specifies the UDP destination port to communicate to the remote GENEVE
+     * tunnel endpoint.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_uint32(properties_override,
+                                              obj_properties,
+                                              NM_SETTING_GENEVE_DESTINATION_PORT,
+                                              PROP_DESTINATION_PORT,
+                                              0,
+                                              G_MAXUINT16,
+                                              DST_PORT_DEFAULT,
+                                              NM_SETTING_PARAM_INFERRABLE,
+                                              NMSettingGenevePrivate,
+                                              destination_port);
+
+    /**
+     * NMSettingGeneve:tos:
+     *
+     * Specifies the TOS value to use in outgoing packets.
+     * The special value "inherit" (1) means inherit from outer packet.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_uint32(properties_override,
+                                              obj_properties,
+                                              NM_SETTING_GENEVE_TOS,
+                                              PROP_TOS,
+                                              0,
+                                              255,
+                                              0,
+                                              NM_SETTING_PARAM_INFERRABLE,
+                                              NMSettingGenevePrivate,
+                                              tos);
+
+    /**
+     * NMSettingGeneve:ttl:
+     *
+     * Specifies the time-to-live value to use in outgoing packets.
+     * The special value "inherit" (-1) means inherit from outer packet, 0 means auto, 1-255 are fixed values.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_int32(properties_override,
+                                             obj_properties,
+                                             NM_SETTING_GENEVE_TTL,
+                                             PROP_TTL,
+                                             -1,
+                                             255,
+                                             0,
+                                             NM_SETTING_PARAM_INFERRABLE,
+                                             NMSettingGenevePrivate,
+                                             ttl);
+
+    /**
+     * NMSettingGeneve:df:
+     *
+     * Specifies how the Don't Fragment (DF) flag should be handled in the outer IP
+     * header of GENEVE tunnel packets.
+     *
+     * %NM_SETTING_GENEVE_DF_UNSET (0): Don't set the DF flag, packets may be fragmented.
+     * %NM_SETTING_GENEVE_DF_SET (1): Always set the DF flag, packets will not be fragmented.
+     * %NM_SETTING_GENEVE_DF_INHERIT (2): Inherit the DF flag from the inner IP header.
+     *
+     * Since: 1.58, 1.56.1
+     **/
+    _nm_setting_property_define_direct_enum(properties_override,
+                                            obj_properties,
+                                            NM_SETTING_GENEVE_DF,
+                                            PROP_DF,
+                                            NM_TYPE_SETTING_GENEVE_DF,
+                                            NM_SETTING_GENEVE_DF_UNSET,
+                                            NM_SETTING_PARAM_INFERRABLE,
+                                            NULL,
+                                            NMSettingGenevePrivate,
+                                            df);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    _nm_setting_class_commit(setting_class,
+                             NM_META_SETTING_TYPE_GENEVE,
+                             NULL,
+                             properties_override,
+                             G_STRUCT_OFFSET(NMSettingGeneve, _priv));
+}
diff --git a/src/libnm-core-impl/nm-setting-ip-config.c b/src/libnm-core-impl/nm-setting-ip-config.c
index 1aecc20c..f4ee094c 100644
--- a/src/libnm-core-impl/nm-setting-ip-config.c
+++ b/src/libnm-core-impl/nm-setting-ip-config.c
@@ -5079,6 +5079,9 @@ routing_rules_to_dbus(_NM_SETT_INFO_PROP_TO_DBUS_FCN_ARGS _nm_nil)
     gboolean                  any = FALSE;
     guint                     i;
 
+    if (!_nm_connection_serialize_non_secret(flags))
+        return NULL;
+
     priv = NM_SETTING_IP_CONFIG_GET_PRIVATE(self);
 
     if (!priv->routing_rules || priv->routing_rules->len == 0)
diff --git a/src/libnm-core-impl/nm-setting.c b/src/libnm-core-impl/nm-setting.c
index 295eabe7..e87f39b2 100644
--- a/src/libnm-core-impl/nm-setting.c
+++ b/src/libnm-core-impl/nm-setting.c
@@ -4523,7 +4523,7 @@ nm_range_from_str(const char *str, GError **error)
     gs_free char *str_free = NULL;
     guint64       start;
     guint64       end = 0;
-    char         *c;
+    const char   *c;
 
     g_return_val_if_fail(str, NULL);
     g_return_val_if_fail(!error || !*error, NULL);
diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h
index b8df4d2e..f74dfc59 100644
--- a/src/libnm-core-intern/nm-core-internal.h
+++ b/src/libnm-core-intern/nm-core-internal.h
@@ -37,6 +37,7 @@
 #include "nm-setting-dcb.h"
 #include "nm-setting-dummy.h"
 #include "nm-setting-generic.h"
+#include "nm-setting-geneve.h"
 #include "nm-setting-gsm.h"
 #include "nm-setting-hsr.h"
 #include "nm-setting-hostname.h"
diff --git a/src/libnm-core-intern/nm-meta-setting-base-impl.h b/src/libnm-core-intern/nm-meta-setting-base-impl.h
index d1535e5e..8f512d09 100644
--- a/src/libnm-core-intern/nm-meta-setting-base-impl.h
+++ b/src/libnm-core-intern/nm-meta-setting-base-impl.h
@@ -121,6 +121,7 @@ typedef enum _nm_packed {
     NM_META_SETTING_TYPE_DUMMY,
     NM_META_SETTING_TYPE_ETHTOOL,
     NM_META_SETTING_TYPE_GENERIC,
+    NM_META_SETTING_TYPE_GENEVE,
     NM_META_SETTING_TYPE_GSM,
     NM_META_SETTING_TYPE_HOSTNAME,
     NM_META_SETTING_TYPE_HSR,
diff --git a/src/libnm-core-public/meson.build b/src/libnm-core-public/meson.build
index 086801d9..2a4f143f 100644
--- a/src/libnm-core-public/meson.build
+++ b/src/libnm-core-public/meson.build
@@ -22,6 +22,7 @@ libnm_core_headers = files(
   'nm-setting-dummy.h',
   'nm-setting-ethtool.h',
   'nm-setting-generic.h',
+  'nm-setting-geneve.h',
   'nm-setting-gsm.h',
   'nm-setting-hsr.h',
   'nm-setting-hostname.h',
diff --git a/src/libnm-core-public/nm-connection.h b/src/libnm-core-public/nm-connection.h
index ff3e0f92..e14e23c2 100644
--- a/src/libnm-core-public/nm-connection.h
+++ b/src/libnm-core-public/nm-connection.h
@@ -206,8 +206,10 @@ NMSettingCdma       *nm_connection_get_setting_cdma(NMConnection *connection);
 NMSettingConnection *nm_connection_get_setting_connection(NMConnection *connection);
 NMSettingDcb        *nm_connection_get_setting_dcb(NMConnection *connection);
 NM_AVAILABLE_IN_1_8
-NMSettingDummy      *nm_connection_get_setting_dummy(NMConnection *connection);
-NMSettingGeneric    *nm_connection_get_setting_generic(NMConnection *connection);
+NMSettingDummy   *nm_connection_get_setting_dummy(NMConnection *connection);
+NMSettingGeneric *nm_connection_get_setting_generic(NMConnection *connection);
+NM_AVAILABLE_IN_1_56_1
+NMSettingGeneve     *nm_connection_get_setting_geneve(NMConnection *connection);
 NMSettingGsm        *nm_connection_get_setting_gsm(NMConnection *connection);
 NMSettingInfiniband *nm_connection_get_setting_infiniband(NMConnection *connection);
 NM_AVAILABLE_IN_1_2
diff --git a/src/libnm-core-public/nm-core-types.h b/src/libnm-core-public/nm-core-types.h
index 617d6b74..81743e31 100644
--- a/src/libnm-core-public/nm-core-types.h
+++ b/src/libnm-core-public/nm-core-types.h
@@ -27,6 +27,7 @@ typedef struct _NMSettingConnection       NMSettingConnection;
 typedef struct _NMSettingDcb              NMSettingDcb;
 typedef struct _NMSettingDummy            NMSettingDummy;
 typedef struct _NMSettingEthtool          NMSettingEthtool;
+typedef struct _NMSettingGeneve           NMSettingGeneve;
 typedef struct _NMSettingGeneric          NMSettingGeneric;
 typedef struct _NMSettingGsm              NMSettingGsm;
 typedef struct _NMSettingHostname         NMSettingHostname;
diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h
index 42bff04a..513d1fa3 100644
--- a/src/libnm-core-public/nm-dbus-interface.h
+++ b/src/libnm-core-public/nm-dbus-interface.h
@@ -36,6 +36,7 @@
 #define NM_DBUS_INTERFACE_DEVICE_BRIDGE        NM_DBUS_INTERFACE_DEVICE ".Bridge"
 #define NM_DBUS_INTERFACE_DEVICE_DUMMY         NM_DBUS_INTERFACE_DEVICE ".Dummy"
 #define NM_DBUS_INTERFACE_DEVICE_GENERIC       NM_DBUS_INTERFACE_DEVICE ".Generic"
+#define NM_DBUS_INTERFACE_DEVICE_GENEVE        NM_DBUS_INTERFACE_DEVICE ".Geneve"
 #define NM_DBUS_INTERFACE_DEVICE_GRE           NM_DBUS_INTERFACE_DEVICE ".Gre"
 #define NM_DBUS_INTERFACE_DEVICE_HSR           NM_DBUS_INTERFACE_DEVICE ".Hsr"
 #define NM_DBUS_INTERFACE_DEVICE_INFINIBAND    NM_DBUS_INTERFACE_DEVICE ".Infiniband"
@@ -250,6 +251,7 @@ typedef enum {
  * @NM_DEVICE_TYPE_LOOPBACK: a loopback interface. Since: 1.42.
  * @NM_DEVICE_TYPE_HSR: A HSR/PRP device. Since: 1.46.
  * @NM_DEVICE_TYPE_IPVLAN: A IPVLAN device. Since: 1.52.
+ * @NM_DEVICE_TYPE_GENEVE: A GENEVE device. Since: 1.58, 1.56.1.
  *
  * #NMDeviceType values indicate the type of hardware represented by a
  * device object.
@@ -290,6 +292,7 @@ typedef enum {
     NM_DEVICE_TYPE_LOOPBACK      = 32,
     NM_DEVICE_TYPE_HSR           = 33,
     NM_DEVICE_TYPE_IPVLAN        = 34,
+    NM_DEVICE_TYPE_GENEVE        = 35,
 } NMDeviceType;
 
 /**
@@ -1246,6 +1249,54 @@ typedef enum /*< flags >*/ {
 } NMDeviceReapplyFlags;
 
 /**
+ * NMDeviceManaged:
+ * @NM_DEVICE_MANAGED_NO: the device is not managed.
+ * @NM_DEVICE_MANAGED_YES: the device is managed.
+ * @NM_DEVICE_MANAGED_RESET: reset the device managed state to the default value.
+ *
+ * Values for the SetManaged() D-Bus call of a device and nm_device_set_managed_async().
+ *
+ * Since: 1.58, 1.56.1
+ */
+typedef enum {
+    NM_DEVICE_MANAGED_NO    = 0,
+    NM_DEVICE_MANAGED_YES   = 1,
+    NM_DEVICE_MANAGED_RESET = 2,
+} NMDeviceManaged;
+
+/**
+ * NMDeviceManagedFlags:
+ * @NM_DEVICE_MANAGED_FLAGS_NONE: no flag set.
+ * @NM_DEVICE_MANAGED_FLAGS_RUNTIME: to set the device managed state to the runtime value.
+ * @NM_DEVICE_MANAGED_FLAGS_PERMANENT: to set the device managed state to the permanent (on disk) value.
+ * @NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME: to match the device by name, not by MAC address.
+ * @NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC: to match the device by MAC address, not by name.
+ * @NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE: to set the administrative state of the
+ *   device to up if the managed state is %NM_DEVICE_MANAGED_YES, and down if the managed state
+ *   is %NM_DEVICE_MANAGED_NO. If the flag is not set, the administrative state is not changed.
+ *   The flag is ignored for %NM_DEVICE_MANAGED_RESET.
+ * @NM_DEVICE_MANAGED_FLAGS_ALL: all flags.
+ *
+ * Flags for the SetManaged() D-Bus call of a device and nm_device_set_managed_async().
+ *
+ * %NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME and %NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC
+ * are mutually exclusive, and they only make sense together with %NM_DEVICE_MANAGED_FLAGS_PERMANENT.
+ * If none is set, the matching criteria is selected automatically.
+ *
+ * Since: 1.58, 1.56.1
+ */
+typedef enum /*< flags >*/ {
+    NM_DEVICE_MANAGED_FLAGS_NONE              = 0,
+    NM_DEVICE_MANAGED_FLAGS_RUNTIME           = 0x1,
+    NM_DEVICE_MANAGED_FLAGS_PERMANENT         = 0x2,
+    NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME = 0x4,
+    NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC  = 0x8,
+    NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE   = 0x10,
+
+    NM_DEVICE_MANAGED_FLAGS_ALL = 0x1F, /* <skip> */
+} NMDeviceManagedFlags;
+
+/**
  * NMTernary:
  * @NM_TERNARY_DEFAULT: use the globally-configured default value.
  * @NM_TERNARY_FALSE: the option is disabled.
diff --git a/src/libnm-core-public/nm-setting-geneve.h b/src/libnm-core-public/nm-setting-geneve.h
new file mode 100644
index 00000000..642697dc
--- /dev/null
+++ b/src/libnm-core-public/nm-setting-geneve.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026 Red Hat, Inc.
+ */
+
+#ifndef __NM_SETTING_GENEVE_H__
+#define __NM_SETTING_GENEVE_H__
+
+#if !defined(__NETWORKMANAGER_H_INSIDE__) && !defined(NETWORKMANAGER_COMPILATION)
+#error "Only <NetworkManager.h> can be included directly."
+#endif
+
+#include "nm-setting.h"
+
+G_BEGIN_DECLS
+
+#define NM_TYPE_SETTING_GENEVE (nm_setting_geneve_get_type())
+#define NM_SETTING_GENEVE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_SETTING_GENEVE, NMSettingGeneve))
+#define NM_SETTING_GENEVE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_SETTING_GENEVE, NMSettingGeneveClass))
+#define NM_IS_SETTING_GENEVE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_SETTING_GENEVE))
+#define NM_IS_SETTING_GENEVE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_SETTING_GENEVE))
+#define NM_SETTING_GENEVE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_SETTING_GENEVE, NMSettingGeneveClass))
+
+#define NM_SETTING_GENEVE_SETTING_NAME "geneve"
+
+#define NM_SETTING_GENEVE_ID               "id"
+#define NM_SETTING_GENEVE_REMOTE           "remote"
+#define NM_SETTING_GENEVE_DESTINATION_PORT "destination-port"
+#define NM_SETTING_GENEVE_TOS              "tos"
+#define NM_SETTING_GENEVE_TTL              "ttl"
+#define NM_SETTING_GENEVE_DF               "df"
+
+/**
+ * NMSettingGeneveDf:
+ * @NM_SETTING_GENEVE_DF_UNSET: Don't set the DF flag, packets may be fragmented.
+ * @NM_SETTING_GENEVE_DF_SET: Always set the DF flag, packets will not be fragmented.
+ * @NM_SETTING_GENEVE_DF_INHERIT: Inherit the DF flag from the inner IP header.
+ *
+ * #NMSettingGeneveDf values indicate how the Don't Fragment (DF) flag should be handled
+ * in the outer IP header of GENEVE tunnel packets.
+ *
+ * Since: 1.58, 1.56.1
+ */
+typedef enum {
+    NM_SETTING_GENEVE_DF_UNSET   = 0,
+    NM_SETTING_GENEVE_DF_SET     = 1,
+    NM_SETTING_GENEVE_DF_INHERIT = 2,
+} NMSettingGeneveDf;
+
+typedef struct _NMSettingGeneveClass NMSettingGeneveClass;
+
+NM_AVAILABLE_IN_1_56_1
+GType nm_setting_geneve_get_type(void);
+NM_AVAILABLE_IN_1_56_1
+NMSetting *nm_setting_geneve_new(void);
+NM_AVAILABLE_IN_1_56_1
+guint nm_setting_geneve_get_id(NMSettingGeneve *setting);
+NM_AVAILABLE_IN_1_56_1
+const char *nm_setting_geneve_get_remote(NMSettingGeneve *setting);
+NM_AVAILABLE_IN_1_56_1
+guint nm_setting_geneve_get_destination_port(NMSettingGeneve *setting);
+NM_AVAILABLE_IN_1_56_1
+guint nm_setting_geneve_get_tos(NMSettingGeneve *setting);
+NM_AVAILABLE_IN_1_56_1
+guint nm_setting_geneve_get_ttl(NMSettingGeneve *setting);
+NM_AVAILABLE_IN_1_56_1
+NMSettingGeneveDf nm_setting_geneve_get_df(NMSettingGeneve *setting);
+
+G_END_DECLS
+
+#endif /* __NM_SETTING_GENEVE_H__ */
diff --git a/src/libnm-core-public/nm-version-macros.h.in b/src/libnm-core-public/nm-version-macros.h.in
index b8b89a44..fdecfa68 100644
--- a/src/libnm-core-public/nm-version-macros.h.in
+++ b/src/libnm-core-public/nm-version-macros.h.in
@@ -79,6 +79,7 @@
 #define NM_VERSION_1_52   (NM_ENCODE_VERSION(1, 52, 0))
 #define NM_VERSION_1_54   (NM_ENCODE_VERSION(1, 54, 0))
 #define NM_VERSION_1_56   (NM_ENCODE_VERSION(1, 56, 0))
+#define NM_VERSION_1_56_1 (NM_ENCODE_VERSION(1, 56, 1))
 
 /* For releases, NM_API_VERSION is equal to NM_VERSION.
  *
diff --git a/src/libnm-core-public/nm-version.h b/src/libnm-core-public/nm-version.h
index af45b679..6e18acdd 100644
--- a/src/libnm-core-public/nm-version.h
+++ b/src/libnm-core-public/nm-version.h
@@ -453,6 +453,12 @@
 #define NM_AVAILABLE_IN_1_56
 #endif
 
+#if NM_VERSION_MAX_ALLOWED < NM_VERSION_1_56_1
+#define NM_AVAILABLE_IN_1_56_1 G_UNAVAILABLE(1, 56.1)
+#else
+#define NM_AVAILABLE_IN_1_56_1
+#endif
+
 /*
  * Synchronous API for calling D-Bus in libnm is deprecated. See
  * https://networkmanager.dev/docs/libnm/latest/usage.html#sync-api
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index 1fca6015..0bc6e294 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -130,6 +130,7 @@ typedef enum {
 #define _NM_LINK_TYPE_SW_FIRST NM_LINK_TYPE_BNEP
     NM_LINK_TYPE_BNEP, /* Bluetooth Ethernet emulation */
     NM_LINK_TYPE_DUMMY,
+    NM_LINK_TYPE_GENEVE,
     NM_LINK_TYPE_GRE,
     NM_LINK_TYPE_GRETAP,
     NM_LINK_TYPE_IFB,
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));
diff --git a/src/libnm-systemd-shared/src/basic/sort-util.h b/src/libnm-systemd-shared/src/basic/sort-util.h
index 9c818bd7..a3bfb32c 100644
--- a/src/libnm-systemd-shared/src/basic/sort-util.h
+++ b/src/libnm-systemd-shared/src/basic/sort-util.h
@@ -25,15 +25,18 @@ void *xbsearch_r(const void *key, const void *base, size_t nmemb, size_t size,
  * Normal bsearch requires base to be nonnull. Here were require
  * that only if nmemb > 0.
  */
-static inline void* bsearch_safe(const void *key, const void *base,
+static inline void* bsearch_safe_internal(const void *key, const void *base,
                                  size_t nmemb, size_t size, comparison_fn_t compar) {
         if (nmemb <= 0)
                 return NULL;
 
         assert(base);
-        return bsearch(key, base, nmemb, size, compar);
+        return (void*) bsearch(key, base, nmemb, size, compar);
 }
 
+#define bsearch_safe(key, base, nmemb, size, compar) \
+        const_generic((base), bsearch_safe_internal(key, base, nmemb, size, compar))
+
 #define typesafe_bsearch(k, b, n, func)                                 \
         ({                                                              \
                 const typeof((b)[0]) *_k = k;                           \
diff --git a/src/libnm-systemd-shared/src/basic/string-util.c b/src/libnm-systemd-shared/src/basic/string-util.c
index 5efd11ea..52160fe3 100644
--- a/src/libnm-systemd-shared/src/basic/string-util.c
+++ b/src/libnm-systemd-shared/src/basic/string-util.c
@@ -1470,7 +1470,7 @@ ssize_t strlevenshtein(const char *x, const char *y) {
         return t1[yl];
 }
 
-char* strrstr(const char *haystack, const char *needle) {
+char* strrstr_internal(const char *haystack, const char *needle) {
         /* Like strstr() but returns the last rather than the first occurrence of "needle" in "haystack". */
 
         if (!haystack || !needle)
@@ -1479,7 +1479,7 @@ char* strrstr(const char *haystack, const char *needle) {
         /* Special case: for the empty string we return the very last possible occurrence, i.e. *after* the
          * last char, not before. */
         if (*needle == 0)
-                return strchr(haystack, 0);
+                return (char*) strchr(haystack, 0);
 
         for (const char *p = strstr(haystack, needle), *q; p; p = q) {
                 q = strstr(p + 1, needle);
diff --git a/src/libnm-systemd-shared/src/basic/string-util.h b/src/libnm-systemd-shared/src/basic/string-util.h
index 91e63c9b..5d2b5dfb 100644
--- a/src/libnm-systemd-shared/src/basic/string-util.h
+++ b/src/libnm-systemd-shared/src/basic/string-util.h
@@ -27,24 +27,28 @@
 #define URI_UNRESERVED      ALPHANUMERICAL "-._~"       /* [RFC3986] */
 #define URI_VALID           URI_RESERVED URI_UNRESERVED /* [RFC3986] */
 
-static inline char* strstr_ptr(const char *haystack, const char *needle) {
+static inline char* strstr_ptr_internal(const char *haystack, const char *needle) {
         if (!haystack || !needle)
                 return NULL;
-        return strstr(haystack, needle);
+        return (char*) strstr(haystack, needle);
 }
 
-static inline char* strstrafter(const char *haystack, const char *needle) {
-        char *p;
+#define strstr_ptr(haystack, needle) \
+        const_generic(haystack, strstr_ptr_internal(haystack, needle))
 
+static inline char* strstrafter_internal(const char *haystack, const char *needle) {
         /* Returns NULL if not found, or pointer to first character after needle if found */
 
-        p = strstr_ptr(haystack, needle);
+        char *p = (char*) strstr_ptr(haystack, needle);
         if (!p)
                 return NULL;
 
         return p + strlen(needle);
 }
 
+#define strstrafter(haystack, needle) \
+        const_generic(haystack, strstrafter_internal(haystack, needle))
+
 static inline const char* strnull(const char *s) {
         return s ?: "(null)";
 }
@@ -300,6 +304,8 @@ bool version_is_valid_versionspec(const char *s);
 
 ssize_t strlevenshtein(const char *x, const char *y);
 
-char* strrstr(const char *haystack, const char *needle);
+char* strrstr_internal(const char *haystack, const char *needle);
+#define strrstr(haystack, needle) \
+        const_generic(haystack, strrstr_internal(haystack, needle))
 
 size_t str_common_prefix(const char *a, const char *b);
diff --git a/src/libnm-systemd-shared/src/fundamental/macro-fundamental.h b/src/libnm-systemd-shared/src/fundamental/macro-fundamental.h
index abdfb9b6..a89994e2 100644
--- a/src/libnm-systemd-shared/src/fundamental/macro-fundamental.h
+++ b/src/libnm-systemd-shared/src/fundamental/macro-fundamental.h
@@ -511,4 +511,11 @@ assert_cc(STRLEN(__FILE__) > STRLEN(RELATIVE_SOURCE_PATH) + 1);
 #define PROJECT_FILE (&__FILE__[STRLEN(RELATIVE_SOURCE_PATH) + 1])
 #else /* NM_IGNORED */
 #define PROJECT_FILE __FILE__
-#endif /* NM_IGNORED */
\ No newline at end of file
+#endif /* NM_IGNORED */
+
+/* This macro is used to have a const-returning and non-const returning version of a function based on
+ * whether its first argument is const or not (e.g. strstr()). */
+#define const_generic(ptr, call)                              \
+        _Generic(0 ? (ptr) : (void*) 1,                       \
+                 const void*: (const typeof(*call)*) (call),  \
+                 void*: (call))
diff --git a/src/libnm-udev-aux/nm-udev-utils.c b/src/libnm-udev-aux/nm-udev-utils.c
index 744d03ce..9e43dd26 100644
--- a/src/libnm-udev-aux/nm-udev-utils.c
+++ b/src/libnm-udev-aux/nm-udev-utils.c
@@ -92,7 +92,8 @@ _subsystem_split(const char  *subsystem_full,
                  const char **out_devtype,
                  char       **to_free)
 {
-    char *tmp, *s;
+    char       *tmp;
+    const char *s;
 
     nm_assert(subsystem_full);
     nm_assert(out_subsystem);
@@ -101,12 +102,12 @@ _subsystem_split(const char  *subsystem_full,
 
     s = strstr(subsystem_full, "/");
     if (s) {
-        tmp            = g_strdup(subsystem_full);
-        s              = &tmp[s - subsystem_full];
-        *s             = '\0';
-        *out_subsystem = tmp;
-        *out_devtype   = &s[1];
-        *to_free       = tmp;
+        tmp                     = g_strdup(subsystem_full);
+        tmp[s - subsystem_full] = '\0';
+        s                       = &tmp[s - subsystem_full];
+        *out_subsystem          = tmp;
+        *out_devtype            = &s[1];
+        *to_free                = tmp;
     } else {
         *out_subsystem = subsystem_full;
         *out_devtype   = NULL;
diff --git a/src/libnmc-setting/nm-meta-setting-base-impl.c b/src/libnmc-setting/nm-meta-setting-base-impl.c
index 52af9ad2..d84490d2 100644
--- a/src/libnmc-setting/nm-meta-setting-base-impl.c
+++ b/src/libnmc-setting/nm-meta-setting-base-impl.c
@@ -27,6 +27,7 @@
 #include "nm-setting-dummy.h"
 #include "nm-setting-ethtool.h"
 #include "nm-setting-generic.h"
+#include "nm-setting-geneve.h"
 #include "nm-setting-gsm.h"
 #include "nm-setting-hostname.h"
 #include "nm-setting-hsr.h"
@@ -324,6 +325,13 @@ const NMMetaSettingInfo nm_meta_setting_infos[] = {
             .setting_name      = NM_SETTING_GENERIC_SETTING_NAME,
             .get_setting_gtype = nm_setting_generic_get_type,
         },
+    [NM_META_SETTING_TYPE_GENEVE] =
+        {
+            .meta_type         = NM_META_SETTING_TYPE_GENEVE,
+            .setting_priority  = NM_SETTING_PRIORITY_HW_BASE,
+            .setting_name      = NM_SETTING_GENEVE_SETTING_NAME,
+            .get_setting_gtype = nm_setting_geneve_get_type,
+        },
     [NM_META_SETTING_TYPE_GSM] =
         {
             .meta_type         = NM_META_SETTING_TYPE_GSM,
@@ -655,6 +663,7 @@ const NMMetaSettingType nm_meta_setting_types_by_priority[] = {
     NM_META_SETTING_TYPE_CDMA,
     NM_META_SETTING_TYPE_DUMMY,
     NM_META_SETTING_TYPE_GENERIC,
+    NM_META_SETTING_TYPE_GENEVE,
     NM_META_SETTING_TYPE_GSM,
     NM_META_SETTING_TYPE_HSR,
     NM_META_SETTING_TYPE_INFINIBAND,
diff --git a/src/libnmc-setting/nm-meta-setting-base-impl.h b/src/libnmc-setting/nm-meta-setting-base-impl.h
index d1535e5e..8f512d09 100644
--- a/src/libnmc-setting/nm-meta-setting-base-impl.h
+++ b/src/libnmc-setting/nm-meta-setting-base-impl.h
@@ -121,6 +121,7 @@ typedef enum _nm_packed {
     NM_META_SETTING_TYPE_DUMMY,
     NM_META_SETTING_TYPE_ETHTOOL,
     NM_META_SETTING_TYPE_GENERIC,
+    NM_META_SETTING_TYPE_GENEVE,
     NM_META_SETTING_TYPE_GSM,
     NM_META_SETTING_TYPE_HOSTNAME,
     NM_META_SETTING_TYPE_HSR,
diff --git a/src/libnmc-setting/nm-meta-setting-desc.c b/src/libnmc-setting/nm-meta-setting-desc.c
index e2ab8dfa..ad75782c 100644
--- a/src/libnmc-setting/nm-meta-setting-desc.c
+++ b/src/libnmc-setting/nm-meta-setting-desc.c
@@ -6112,6 +6112,57 @@ static const NMMetaPropertyInfo *const property_infos_GENERIC[] = {
 };
 
 #undef  _CURRENT_NM_META_SETTING_TYPE
+#define _CURRENT_NM_META_SETTING_TYPE NM_META_SETTING_TYPE_GENEVE
+static const NMMetaPropertyInfo *const property_infos_GENEVE[] = {
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_ID,
+        .is_cli_option =                TRUE,
+        .property_alias =               "id",
+        .inf_flags =                    NM_META_PROPERTY_INF_FLAG_REQD,
+        .prompt =                       N_("GENEVE ID"),
+        .property_type =                &_pt_gobject_int,
+    ),
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_REMOTE,
+        .is_cli_option =                TRUE,
+        .property_alias =               "remote",
+        .inf_flags =                    NM_META_PROPERTY_INF_FLAG_REQD,
+        .prompt =                       N_("Remote"),
+        .property_type =                &_pt_gobject_string,
+    ),
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_DESTINATION_PORT,
+        .is_cli_option =                TRUE,
+        .property_alias =               "destination-port",
+        .prompt =                       N_("Destination port"),
+        .property_type =                &_pt_gobject_int,
+    ),
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_TOS,
+        .property_type =                &_pt_gobject_int,
+        .property_typ_data = DEFINE_PROPERTY_TYP_DATA_SUBTYPE (gobject_int,
+            .value_infos =              INT_VALUE_INFOS (
+                {
+                    .value.u64 = 1,
+                    .nick = "inherit",
+                },
+            ),
+        ),
+    ),
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_TTL,
+        .property_type =                &_pt_gobject_int,
+        .property_typ_data = DEFINE_PROPERTY_TYP_DATA_SUBTYPE (gobject_int,
+            .value_infos =              INT_VALUE_INFOS (
+                {
+                    .value.i64 = -1,
+                    .nick = "inherit",
+                },
+            ),
+        ),
+    ),
+    PROPERTY_INFO_WITH_DESC (NM_SETTING_GENEVE_DF,
+        .property_type =                &_pt_gobject_enum,
+    ),
+    NULL
+};
+
+#undef  _CURRENT_NM_META_SETTING_TYPE
 #define _CURRENT_NM_META_SETTING_TYPE NM_META_SETTING_TYPE_GSM
 static const NMMetaPropertyInfo *const property_infos_GSM[] = {
     PROPERTY_INFO_WITH_DESC (NM_SETTING_GSM_AUTO_CONFIG,
@@ -9014,6 +9065,7 @@ _setting_init_fcn_wireless (ARGS_SETTING_INIT_FCN)
 #define SETTING_PRETTY_NAME_DUMMY               N_("Dummy settings")
 #define SETTING_PRETTY_NAME_ETHTOOL             N_("Ethtool settings")
 #define SETTING_PRETTY_NAME_GENERIC             N_("Generic settings")
+#define SETTING_PRETTY_NAME_GENEVE              N_("Geneve settings")
 #define SETTING_PRETTY_NAME_GSM                 N_("GSM mobile broadband connection")
 #define SETTING_PRETTY_NAME_HOSTNAME            N_("Hostname settings")
 #define SETTING_PRETTY_NAME_HSR                 N_("HSR settings")
@@ -9152,6 +9204,14 @@ const NMMetaSettingInfoEditor nm_meta_setting_infos_editor[] = {
             NM_META_SETTING_VALID_PART_ITEM (GENERIC,               TRUE),
         ),
     ),
+    SETTING_INFO (GENEVE,
+        .valid_parts = NM_META_SETTING_VALID_PARTS (
+            NM_META_SETTING_VALID_PART_ITEM (CONNECTION,            TRUE),
+            NM_META_SETTING_VALID_PART_ITEM (GENEVE,                 TRUE),
+            NM_META_SETTING_VALID_PART_ITEM (WIRED,                 FALSE),
+            NM_META_SETTING_VALID_PART_ITEM (ETHTOOL,               FALSE),
+        ),
+    ),
     SETTING_INFO (GSM,
         .valid_parts = NM_META_SETTING_VALID_PARTS (
             NM_META_SETTING_VALID_PART_ITEM (CONNECTION,            TRUE),
diff --git a/src/libnmc-setting/settings-docs.h.in b/src/libnmc-setting/settings-docs.h.in
index 89f8c6d1..17468497 100644
--- a/src/libnmc-setting/settings-docs.h.in
+++ b/src/libnmc-setting/settings-docs.h.in
@@ -498,6 +498,12 @@
 #define DESCRIBE_DOC_NM_SETTING_WPAN_SHORT_ADDRESS N_("Short IEEE 802.15.4 address to be used within a restricted environment.")
 #define DESCRIBE_DOC_NM_SETTING_BOND_PORT_PRIO N_("The port priority for bond active port re-selection during failover. A higher number means a higher priority in selection. The primary port has the highest priority. This option is only compatible with active-backup, balance-tlb and balance-alb modes.")
 #define DESCRIBE_DOC_NM_SETTING_BOND_PORT_QUEUE_ID N_("The queue ID of this bond port. The maximum value of queue ID is the number of TX queues currently active in device.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_DESTINATION_PORT N_("Specifies the UDP destination port to communicate to the remote GENEVE tunnel endpoint.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_DF N_("Specifies how the Don't Fragment (DF) flag should be handled in the outer IP header of GENEVE tunnel packets. \"unset\" (0) (0): Don't set the DF flag, packets may be fragmented. \"set\" (1) (1): Always set the DF flag, packets will not be fragmented. \"inherit\" (2) (2): Inherit the DF flag from the inner IP header.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_ID N_("Specifies the GENEVE Network Identifier (or GENEVE Segment Identifier) to use.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_REMOTE N_("Specifies the unicast destination IP address to use in outgoing packets when communicating with the remote GENEVE tunnel endpoint.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_TOS N_("Specifies the TOS value to use in outgoing packets. The special value \"inherit\" (1) means inherit from outer packet.")
+#define DESCRIBE_DOC_NM_SETTING_GENEVE_TTL N_("Specifies the time-to-live value to use in outgoing packets. The special value \"inherit\" (-1) means inherit from outer packet, 0 means auto, 1-255 are fixed values.")
 #define DESCRIBE_DOC_NM_SETTING_HOSTNAME_FROM_DHCP N_("Whether the system hostname can be determined from DHCP on this connection. When set to \"default\" (-1), the value from global configuration is used. If the property doesn't have a value in the global configuration, NetworkManager assumes the value to be \"true\" (1).")
 #define DESCRIBE_DOC_NM_SETTING_HOSTNAME_FROM_DNS_LOOKUP N_("Whether the system hostname can be determined from reverse DNS lookup of addresses on this device. When set to \"default\" (-1), the value from global configuration is used. If the property doesn't have a value in the global configuration, NetworkManager assumes the value to be \"true\" (1).")
 #define DESCRIBE_DOC_NM_SETTING_HOSTNAME_ONLY_FROM_DEFAULT N_("If set to \"true\" (1), NetworkManager attempts to get the hostname via DHCPv4/DHCPv6 or reverse DNS lookup on this device only when the device has the default route for the given address family (IPv4/IPv6). If set to \"false\" (0), the hostname can be set from this device even if it doesn't have the default route. When set to \"default\" (-1), the value from global configuration is used. If the property doesn't have a value in the global configuration, NetworkManager assumes the value to be \"false\" (0).")
diff --git a/src/nm-initrd-generator/nmi-cmdline-reader.c b/src/nm-initrd-generator/nmi-cmdline-reader.c
index 5cdb3bcd..f9b0fa16 100644
--- a/src/nm-initrd-generator/nmi-cmdline-reader.c
+++ b/src/nm-initrd-generator/nmi-cmdline-reader.c
@@ -1177,7 +1177,7 @@ reader_parse_rd_znet(Reader *reader, char *argument, gboolean net_ifnames)
 {
     const char     *nettype;
     const char     *subchannels[4] = {0, 0, 0, 0};
-    const char     *tmp;
+    char           *tmp;
     gs_free char   *ifname          = NULL;
     gs_free char   *str_subchannels = NULL;
     const char     *prefix;
@@ -1248,8 +1248,8 @@ reader_parse_rd_znet(Reader *reader, char *argument, gboolean net_ifnames)
                  NULL);
 
     while ((tmp = get_word(&argument, ',')) != NULL) {
-        const char *key;
-        char       *val;
+        char *key;
+        char *val;
 
         val = strchr(tmp, '=');
         if (!val) {
diff --git a/src/nmcli/connections.c b/src/nmcli/connections.c
index d93e16f0..a3b03efe 100644
--- a/src/nmcli/connections.c
+++ b/src/nmcli/connections.c
@@ -4,6 +4,7 @@
  */
 
 #include "libnm-client-aux-extern/nm-default-client.h"
+#include "nmcli.h"
 
 #include "connections.h"
 
@@ -1070,13 +1071,13 @@ const NmcMetaGenericInfo
     "," NM_SETTING_DCB_SETTING_NAME "," NM_SETTING_TUN_SETTING_NAME                    \
     "," NM_SETTING_IP_TUNNEL_SETTING_NAME "," NM_SETTING_MACSEC_SETTING_NAME           \
     "," NM_SETTING_MACVLAN_SETTING_NAME "," NM_SETTING_VXLAN_SETTING_NAME              \
-    "," NM_SETTING_VRF_SETTING_NAME "," NM_SETTING_WPAN_SETTING_NAME                   \
-    "," NM_SETTING_6LOWPAN_SETTING_NAME "," NM_SETTING_WIREGUARD_SETTING_NAME          \
-    "," NM_SETTING_LINK_SETTING_NAME "," NM_SETTING_PROXY_SETTING_NAME                 \
-    "," NM_SETTING_TC_CONFIG_SETTING_NAME "," NM_SETTING_SRIOV_SETTING_NAME            \
-    "," NM_SETTING_ETHTOOL_SETTING_NAME "," NM_SETTING_OVS_DPDK_SETTING_NAME           \
-    "," NM_SETTING_HOSTNAME_SETTING_NAME "," NM_SETTING_HSR_SETTING_NAME               \
-    "," NM_SETTING_IPVLAN_SETTING_NAME
+    "," NM_SETTING_GENEVE_SETTING_NAME "," NM_SETTING_VRF_SETTING_NAME                 \
+    "," NM_SETTING_WPAN_SETTING_NAME "," NM_SETTING_6LOWPAN_SETTING_NAME               \
+    "," NM_SETTING_WIREGUARD_SETTING_NAME "," NM_SETTING_LINK_SETTING_NAME             \
+    "," NM_SETTING_PROXY_SETTING_NAME "," NM_SETTING_TC_CONFIG_SETTING_NAME            \
+    "," NM_SETTING_SRIOV_SETTING_NAME "," NM_SETTING_ETHTOOL_SETTING_NAME              \
+    "," NM_SETTING_OVS_DPDK_SETTING_NAME "," NM_SETTING_HOSTNAME_SETTING_NAME          \
+    "," NM_SETTING_HSR_SETTING_NAME "," NM_SETTING_IPVLAN_SETTING_NAME
 /* NM_SETTING_DUMMY_SETTING_NAME NM_SETTING_WIMAX_SETTING_NAME */
 
 const NmcMetaGenericInfo *const nmc_fields_con_active_details_groups[] = {
@@ -1306,6 +1307,9 @@ usage_connection_add(void)
           "                  [source-port-min <0-65535>]\n"
           "                  [source-port-max <0-65535>]\n"
           "                  [destination-port <0-65535>]\n\n"
+          "    geneve:       id <GENEVE ID>\n"
+          "                  remote <IP address>\n"
+          "                  [destination-port <0-65535>]\n\n"
           "    wpan:         [short-addr <0x0000-0xffff>]\n"
           "                  [pan-id <0x0000-0xffff>]\n"
           "                  [page <default|0-31>]\n"
diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c
index e81e710f..2787439e 100644
--- a/src/nmcli/devices.c
+++ b/src/nmcli/devices.c
@@ -854,7 +854,8 @@ usage(void)
           "delete | monitor | wifi | lldp }\n\n"
           "  status\n\n"
           "  show [<ifname>]\n\n"
-          "  set [ifname] <ifname> [autoconnect yes|no] [managed yes|no]\n\n"
+          "  set [ifname] <ifname> [autoconnect yes|no] [managed [--permanent|--permanent-only] "
+          "yes|no|up|down|reset]\n\n"
           "  connect <ifname>\n\n"
           "  reapply <ifname>\n\n"
           "  modify <ifname> ([+|-]<setting>.<property> <value>)+\n\n"
@@ -972,14 +973,15 @@ usage_device_delete(void)
 static void
 usage_device_set(void)
 {
-    nmc_printerr(_("Usage: nmcli device set { ARGUMENTS | help }\n"
-                   "\n"
-                   "ARGUMENTS := DEVICE { PROPERTY [ PROPERTY ... ] }\n"
-                   "DEVICE    := [ifname] <ifname> \n"
-                   "PROPERTY  := { autoconnect { yes | no } |\n"
-                   "             { managed { yes | no }\n"
-                   "\n"
-                   "Modify device properties.\n\n"));
+    nmc_printerr(_(
+        "Usage: nmcli device set { ARGUMENTS | help }\n"
+        "\n"
+        "ARGUMENTS := DEVICE { PROPERTY [ PROPERTY ... ] }\n"
+        "DEVICE    := [ifname] <ifname> \n"
+        "PROPERTY  := { autoconnect { yes | no } |\n"
+        "             { managed [--permanent | --permanent-only] { yes | no | up | down | reset }\n"
+        "\n"
+        "Modify device properties.\n\n"));
 }
 
 static void
@@ -2793,20 +2795,62 @@ do_devices_delete(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const
     }
 }
 
+typedef struct {
+    NmCli   *nmc;
+    GSource *timeout_source;
+} DeviceSetCbInfo;
+
+static void
+device_set_cb_info_finish(DeviceSetCbInfo *info)
+{
+    nm_clear_g_source_inst(&info->timeout_source);
+    g_slice_free(DeviceSetCbInfo, info);
+    quit();
+}
+
+static gboolean
+device_set_timeout_cb(gpointer user_data)
+{
+    DeviceSetCbInfo *cb_info = user_data;
+
+    timeout_cb(cb_info->nmc);
+    device_set_cb_info_finish(cb_info);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+device_set_cb(GObject *object, GAsyncResult *result, gpointer user_data)
+{
+    NMDevice             *device = NM_DEVICE(object);
+    DeviceSetCbInfo      *info   = (DeviceSetCbInfo *) user_data;
+    NmCli                *nmc    = info->nmc;
+    gs_free_error GError *error  = NULL;
+
+    /* Only 'managed' is treated asynchronously, 'autoconnect' is treated synchronously */
+    if (!nm_device_set_managed_finish(device, result, &error)) {
+        g_string_printf(nmc->return_text, _("Error: set managed failed: %s."), error->message);
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+    }
+
+    device_set_cb_info_finish(info);
+}
+
 static void
 do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
 {
-#define DEV_SET_AUTOCONNECT 0
-#define DEV_SET_MANAGED     1
-    NMDevice *device = NULL;
-    int       i;
+    NMDevice        *device  = NULL;
+    DeviceSetCbInfo *cb_info = NULL;
+    int              i;
     struct {
         int      idx;
         gboolean value;
-    } values[2] = {
-        [DEV_SET_AUTOCONNECT] = {-1},
-        [DEV_SET_MANAGED]     = {-1},
-    };
+    } autoconnect_data = {-1};
+    struct {
+        int             idx;
+        NMDeviceManaged value;
+        guint32         flags;
+    } managed_data = {-1};
+
     gs_free_error GError *error = NULL;
 
     next_arg(nmc, &argc, &argv, NULL);
@@ -2828,49 +2872,120 @@ do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *ar
 
     i = 0;
     do {
-        gboolean flag;
-
         if (argc == 1 && nmc->complete)
             nmc_complete_strings(*argv, "managed", "autoconnect");
 
         if (matches(*argv, "managed")) {
+            NMDeviceManaged val;
+            guint32         flags = 0;
+            gboolean        val_bool;
+            gboolean        perm = FALSE, perm_only = FALSE;
+
             argc--;
             argv++;
+
+            if (argc == 1 && nmc->complete) {
+                nmc_complete_strings(*argv,
+                                     "true",
+                                     "yes",
+                                     "on",
+                                     "false",
+                                     "no",
+                                     "off",
+                                     "up",
+                                     "down",
+                                     "reset",
+                                     "--permanent",
+                                     "--permanent-only");
+            }
+
+            if (managed_data.idx != -1) {
+                g_string_printf(nmc->return_text, _("Error: 'managed' can only be set once."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
+            while (argc > 0) {
+                /* --perm matches with --permanent, the most common,
+                 * but --permanent-only requires a exact match */
+                if (nm_streq0(*argv, "--permanent-only"))
+                    perm_only = TRUE;
+                else if (matches(*argv, "--permanent"))
+                    perm = TRUE;
+                else
+                    break;
+                argc--;
+                argv++;
+            }
+
+            if (perm_only && perm) {
+                g_string_printf(
+                    nmc->return_text,
+                    _("Error: '--permanent-only' and '--permanent' cannot be used together."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            } else if (perm) {
+                flags |= NM_DEVICE_MANAGED_FLAGS_RUNTIME | NM_DEVICE_MANAGED_FLAGS_PERMANENT;
+            } else if (perm_only) {
+                flags |= NM_DEVICE_MANAGED_FLAGS_PERMANENT;
+            } else {
+                /* If --permanent/--permanent-only are missing, set only the runtime flag, as this
+                 * is how it used to work when these options didn't exist. */
+                flags |= NM_DEVICE_MANAGED_FLAGS_RUNTIME;
+            }
+
             if (!argc) {
-                g_string_printf(nmc->return_text,
-                                _("Error: '%s' argument is missing."),
-                                *(argv - 1));
+                g_string_printf(nmc->return_text, _("Error: 'managed' argument is missing."));
                 nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
                 return;
             }
-            if (argc == 1 && nmc->complete)
-                nmc_complete_bool(*argv);
-            if (!nmc_string_to_bool(*argv, &flag, &error)) {
-                g_string_printf(nmc->return_text, _("Error: 'managed': %s."), error->message);
+
+            if (matches(*argv, "up") || matches(*argv, "down")) {
+                flags |= NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE;
+                val = matches(*argv, "up") ? NM_DEVICE_MANAGED_YES : NM_DEVICE_MANAGED_NO;
+            } else if (matches(*argv, "reset")) {
+                val = NM_DEVICE_MANAGED_RESET;
+            } else if (nmc_string_to_bool(*argv, &val_bool, NULL)) {
+                val = val_bool ? NM_DEVICE_MANAGED_YES : NM_DEVICE_MANAGED_NO;
+            } else {
+                g_string_printf(
+                    nmc->return_text,
+                    _("Error: 'managed': '%s' is not valid, use 'yes/no/up/down/reset'."),
+                    *argv);
                 nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
                 return;
             }
-            values[DEV_SET_MANAGED].idx   = ++i;
-            values[DEV_SET_MANAGED].value = flag;
+
+            managed_data.idx   = i++;
+            managed_data.value = val;
+            managed_data.flags = flags;
         } else if (matches(*argv, "autoconnect")) {
+            gboolean val;
+
             argc--;
             argv++;
+
             if (!argc) {
-                g_string_printf(nmc->return_text,
-                                _("Error: '%s' argument is missing."),
-                                *(argv - 1));
+                g_string_printf(nmc->return_text, _("Error: 'autoconnect' argument is missing."));
                 nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
                 return;
             }
+            if (autoconnect_data.idx != -1) {
+                g_string_printf(nmc->return_text, _("Error: 'autoconnect' can only be set once."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
             if (argc == 1 && nmc->complete)
                 nmc_complete_bool(*argv);
-            if (!nmc_string_to_bool(*argv, &flag, &error)) {
+
+            if (!nmc_string_to_bool(*argv, &val, &error)) {
                 g_string_printf(nmc->return_text, _("Error: 'autoconnect': %s."), error->message);
                 nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
                 return;
             }
-            values[DEV_SET_AUTOCONNECT].idx   = ++i;
-            values[DEV_SET_AUTOCONNECT].value = flag;
+            autoconnect_data.idx   = i++;
+            autoconnect_data.value = val;
         } else {
             g_string_printf(nmc->return_text, _("Error: property '%s' is not known."), *argv);
             nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
@@ -2883,15 +2998,28 @@ do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *ar
 
     /* when multiple properties are specified, set them in the order as they
      * are specified on the command line. */
-    if (values[DEV_SET_AUTOCONNECT].idx >= 0 && values[DEV_SET_MANAGED].idx >= 0
-        && values[DEV_SET_MANAGED].idx < values[DEV_SET_AUTOCONNECT].idx) {
-        nm_device_set_managed(device, values[DEV_SET_MANAGED].value);
-        values[DEV_SET_MANAGED].idx = -1;
-    }
-    if (values[DEV_SET_AUTOCONNECT].idx >= 0)
-        nm_device_set_autoconnect(device, values[DEV_SET_AUTOCONNECT].value);
-    if (values[DEV_SET_MANAGED].idx >= 0)
-        nm_device_set_managed(device, values[DEV_SET_MANAGED].value);
+    for (i = 0; i < 2; i++) {
+        if (autoconnect_data.idx == i) {
+            nm_device_set_autoconnect(device, autoconnect_data.value);
+        }
+        if (managed_data.idx == i) {
+            cb_info      = g_slice_new0(DeviceSetCbInfo);
+            cb_info->nmc = nmc;
+            if (nmc->timeout > 0)
+                cb_info->timeout_source =
+                    nm_g_timeout_add_seconds_source(nmc->timeout, device_set_timeout_cb, cb_info);
+
+            nmc->nowait_flag = (nmc->timeout == 0);
+            nmc->should_wait++;
+
+            nm_device_set_managed_async(device,
+                                        managed_data.value,
+                                        managed_data.flags,
+                                        NULL,
+                                        device_set_cb,
+                                        cb_info);
+        }
+    }
 }
 
 static void
diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
index 881c41ca..7a9f4e9c 100644
--- a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
+++ b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in
@@ -629,7 +629,7 @@
                   alias="type"
                   nmcli-description="Base type of the connection. For hardware-dependent connections, should contain the setting name of the hardware-type specific setting (ie, &quot;802-3-ethernet&quot; or &quot;802-11-wireless&quot; or &quot;bluetooth&quot;, etc), and for non-hardware dependent connections like VPN or otherwise, should contain the setting name of that setting type (ie, &quot;vpn&quot; or &quot;bridge&quot;, etc)."
                   format="string"
-                  values="6lowpan, 802-11-olpc-mesh, 802-11-wireless, 802-3-ethernet, adsl, bluetooth, bond, bridge, cdma, dummy, generic, gsm, hsr, infiniband, ip-tunnel, ipvlan, loopback, macsec, macvlan, ovs-bridge, ovs-interface, ovs-port, pppoe, team, tun, veth, vlan, vpn, vrf, vxlan, wifi-p2p, wimax, wireguard, wpan" />
+                  values="6lowpan, 802-11-olpc-mesh, 802-11-wireless, 802-3-ethernet, adsl, bluetooth, bond, bridge, cdma, dummy, generic, geneve, gsm, hsr, infiniband, ip-tunnel, ipvlan, loopback, macsec, macvlan, ovs-bridge, ovs-interface, ovs-port, pppoe, team, tun, veth, vlan, vpn, vrf, vxlan, wifi-p2p, wimax, wireguard, wpan" />
         <property name="interface-name"
                   alias="ifname"
                   nmcli-description="The name of the network interface this connection is bound to. If not set, then the connection can be attached to any interface of the appropriate type (subject to restrictions imposed by other settings). For software devices this specifies the name of the created device. For connection types where interface names cannot easily be made persistent (e.g. mobile broadband or USB Ethernet), this property should not be used. Setting this property restricts the interfaces a connection can be used with, and if interface names change or are reordered the connection may be applied to the wrong interface."
@@ -1110,6 +1110,36 @@
                   nmcli-description="Name of the device handler that will be invoked to add and delete the device for this connection. The name can only contain ASCII alphanumeric characters and &apos;-&apos;, &apos;_&apos;, &apos;.&apos;. It cannot start with &apos;.&apos;. See the NetworkManager-dispatcher(8) man page for more details about how to write the device handler. By setting this property the generic connection becomes &quot;virtual&quot;, meaning that it can be activated without an existing device; the device will be created at the time the connection is started by invoking the device-handler."
                   format="string" />
     </setting>
+    <setting name="geneve" >
+        <property name="id"
+                  alias="id"
+                  nmcli-description="Specifies the GENEVE Network Identifier (or GENEVE Segment Identifier) to use."
+                  format="integer"
+                  values="0 - 16777215" />
+        <property name="remote"
+                  alias="remote"
+                  nmcli-description="Specifies the unicast destination IP address to use in outgoing packets when communicating with the remote GENEVE tunnel endpoint."
+                  format="string" />
+        <property name="destination-port"
+                  alias="destination-port"
+                  nmcli-description="Specifies the UDP destination port to communicate to the remote GENEVE tunnel endpoint."
+                  format="integer"
+                  values="0 - 65535" />
+        <property name="tos"
+                  nmcli-description="Specifies the TOS value to use in outgoing packets. The special value &quot;inherit&quot; (1) means inherit from outer packet."
+                  format="integer"
+                  values="0 - 255"
+                  special-values="inherit (1)" />
+        <property name="ttl"
+                  nmcli-description="Specifies the time-to-live value to use in outgoing packets. The special value &quot;inherit&quot; (-1) means inherit from outer packet, 0 means auto, 1-255 are fixed values."
+                  format="integer"
+                  values="-1 - 255"
+                  special-values="inherit (-1)" />
+        <property name="df"
+                  nmcli-description="Specifies how the Don&apos;t Fragment (DF) flag should be handled in the outer IP header of GENEVE tunnel packets. &quot;unset&quot; (0) (0): Don&apos;t set the DF flag, packets may be fragmented. &quot;set&quot; (1) (1): Always set the DF flag, packets will not be fragmented. &quot;inherit&quot; (2) (2): Inherit the DF flag from the inner IP header."
+                  format="choice (NMSettingGeneveDf)"
+                  values="unset (0), set (1), inherit (2)" />
+    </setting>
     <setting name="gsm" >
         <property name="auto-config"
                   nmcli-description="When TRUE, the settings such as APN, username, or password will default to values that match the network the modem will register to in the Mobile Broadband Provider database."
diff --git a/vapi/NM-1.0.metadata b/vapi/NM-1.0.metadata
index 4f630538..3084b7c2 100644
--- a/vapi/NM-1.0.metadata
+++ b/vapi/NM-1.0.metadata
@@ -29,6 +29,7 @@ SETTING_DNS_OPTION_*        parent="NM.SettingDnsOption"            name="SETTIN
 SETTING_DUMMY_*             parent="NM.SettingDummy"                name="SETTING_DUMMY_(.+)"
 SETTING_ETHTOOL_*           parent="NM.SettingEthtool"              name="SETTING_ETHTOOL_(.+)"
 SETTING_GENERIC_*           parent="NM.SettingGeneric"              name="SETTING_GENERIC_(.+)"
+SETTING_GENEVE_*            parent="NM.SettingGeneve"               name="SETTING_GENEVE_(.+)"
 SETTING_GSM_*               parent="NM.SettingGsm"                  name="SETTING_GSM_(.+)"
 SETTING_HOSTNAME_*          parent="NM.SettingHostname"             name="SETTING_HOSTNAME_(.+)"
 SETTING_HSR_*               parent="NM.SettingHsr"                  name="SETTING_HSR_(.+)"
@@ -114,6 +115,7 @@ DEVICE_BT_*                 parent="NM.DeviceBt"                    name="DEVICE
 DEVICE_DUMMY_*              parent="NM.DeviceDummy"                 name="DEVICE_DUMMY_(.+)"
 DEVICE_ETHERNET_*           parent="NM.DeviceEthernet"              name="DEVICE_ETHERNET_(.+)"
 DEVICE_GENERIC_*            parent="NM.DeviceGeneric"               name="DEVICE_GENERIC_(.+)"
+DEVICE_GENEVE_*             parent="NM.DeviceGeneve"                name="DEVICE_GENEVE_(.+)"
 DEVICE_HSR_*                parent="NM.DeviceHsr"                   name="DEVICE_HSR_(.+)"
 DEVICE_INFINIBAND_*         parent="NM.DeviceInfiniband"            name="DEVICE_INFINIBAND_(.+)"
 DEVICE_IP_TUNNEL_*          parent="NM.DeviceIPTunnel"              name="DEVICE_IP_TUNNEL_(.+)"