about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/devices/nm-device-factory.c17
-rw-r--r--src/core/devices/nm-device-factory.h2
-rw-r--r--src/core/devices/nm-device-generic.c343
-rw-r--r--src/core/devices/nm-device-generic.h3
-rw-r--r--src/core/devices/nm-device-macsec.c17
-rw-r--r--src/core/devices/nm-device-utils.c4
-rw-r--r--src/core/devices/nm-device.c62
-rw-r--r--src/core/dhcp/nm-dhcp-client.c6
-rw-r--r--src/core/ndisc/nm-ndisc.c10
-rw-r--r--src/core/nm-config.c29
-rw-r--r--src/core/nm-config.h5
-rw-r--r--src/core/nm-dispatcher.c677
-rw-r--r--src/core/nm-dispatcher.h16
-rw-r--r--src/core/nm-manager.c18
-rw-r--r--src/core/platform/tests/test-link.c12
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c109
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c2
-rw-r--r--src/core/supplicant/nm-supplicant-config.c30
-rw-r--r--src/core/supplicant/nm-supplicant-config.h7
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c1
21 files changed, 1004 insertions, 368 deletions
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index c97fbb57..69c2a38f 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -28,6 +28,10 @@ G_DEFINE_ABSTRACT_TYPE(NMDeviceFactory, nm_device_factory, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+static NMDeviceFactory *generic_factory;
+
+/*****************************************************************************/
+
 static void
 nm_device_factory_get_supported_types(NMDeviceFactory    *factory,
                                       const NMLinkType  **out_link_types,
@@ -66,7 +70,8 @@ nm_device_factory_create_device(NMDeviceFactory      *factory,
     if (plink) {
         g_return_val_if_fail(!connection, NULL);
         g_return_val_if_fail(strcmp(iface, plink->name) == 0, NULL);
-        nm_assert(factory == nm_device_factory_manager_find_factory_for_link_type(plink->type));
+        nm_assert(factory == nm_device_factory_manager_find_factory_for_link_type(plink->type)
+                  || factory == generic_factory);
     } else if (connection)
         nm_assert(factory == nm_device_factory_manager_find_factory_for_connection(connection));
     else
@@ -185,6 +190,12 @@ static void __attribute__((destructor)) _cleanup(void)
 }
 
 NMDeviceFactory *
+nm_device_factory_get_generic_factory(void)
+{
+    return generic_factory;
+}
+
+NMDeviceFactory *
 nm_device_factory_manager_find_factory_for_link_type(NMLinkType link_type)
 {
     g_return_val_if_fail(factories_by_link, NULL);
@@ -300,9 +311,12 @@ _load_internal_factory(GType                             factory_gtype,
                        gpointer                          user_data)
 {
     gs_unref_object NMDeviceFactory *factory = NULL;
+    GType                            nm_generic_device_factory_get_type(void);
 
     factory = g_object_new(factory_gtype, NULL);
     _add_factory(factory, NULL, callback, user_data);
+    if (factory_gtype == nm_generic_device_factory_get_type())
+        generic_factory = factory;
 }
 
 static void
@@ -396,6 +410,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb
     _ADD_INTERNAL(nm_bridge_device_factory_get_type);
     _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_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-factory.h b/src/core/devices/nm-device-factory.h
index fc3d9dd4..004ae9b1 100644
--- a/src/core/devices/nm-device-factory.h
+++ b/src/core/devices/nm-device-factory.h
@@ -234,4 +234,6 @@ NMDeviceFactory *nm_device_factory_manager_find_factory_for_connection(NMConnect
 void nm_device_factory_manager_for_each_factory(NMDeviceFactoryManagerFactoryFunc callback,
                                                 gpointer                          user_data);
 
+NMDeviceFactory *nm_device_factory_get_generic_factory(void);
+
 #endif /* __NETWORKMANAGER_DEVICE_FACTORY_H__ */
diff --git a/src/core/devices/nm-device-generic.c b/src/core/devices/nm-device-generic.c
index ead671d4..85f65246 100644
--- a/src/core/devices/nm-device-generic.c
+++ b/src/core/devices/nm-device-generic.c
@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 /*
- * Copyright (C) 2013 Red Hat, Inc.
+ * Copyright (C) 2013-2023 Red Hat, Inc.
  */
 
 #include "src/core/nm-default-daemon.h"
@@ -10,13 +10,27 @@
 #include "nm-device-private.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-core-intern/nm-core-internal.h"
+#include "nm-dispatcher.h"
+#include "nm-device-factory.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceGeneric
+#include "devices/nm-device-logging.h"
 
 /*****************************************************************************/
 
-NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_TYPE_DESCRIPTION, );
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceGeneric, PROP_TYPE_DESCRIPTION, PROP_HAS_DEVICE_HANDLER, );
 
 typedef struct {
-    const char *type_description;
+    const char         *type_description;
+    bool                prepare_done : 1;
+    bool                has_device_handler : 1;
+    NMDispatcherCallId *dispatcher_call_id;
+    struct {
+        NMDeviceDeactivateCallback callback;
+        gpointer                   callback_data;
+        GCancellable              *cancellable;
+        gulong                     cancellable_id;
+    } deactivate;
 } NMDeviceGenericPrivate;
 
 struct _NMDeviceGeneric {
@@ -38,13 +52,151 @@ G_DEFINE_TYPE(NMDeviceGeneric, nm_device_generic, NM_TYPE_DEVICE)
 static NMDeviceCapabilities
 get_generic_capabilities(NMDevice *device)
 {
-    int ifindex = nm_device_get_ifindex(device);
+    NMDeviceGenericPrivate *priv    = NM_DEVICE_GENERIC_GET_PRIVATE(device);
+    int                     ifindex = nm_device_get_ifindex(device);
+    NMDeviceCapabilities    cap     = NM_DEVICE_CAP_NONE;
+
+    if (priv->has_device_handler)
+        cap |= NM_DEVICE_CAP_IS_SOFTWARE;
 
     if (ifindex > 0
         && nm_platform_link_supports_carrier_detect(nm_device_get_platform(device), ifindex))
-        return NM_DEVICE_CAP_CARRIER_DETECT;
-    else
-        return NM_DEVICE_CAP_NONE;
+        cap |= NM_DEVICE_CAP_CARRIER_DETECT;
+
+    return cap;
+}
+
+static void
+device_add_dispatcher_cb(NMDispatcherCallId *call_id,
+                         gpointer            user_data,
+                         gboolean            success,
+                         const char         *error,
+                         GHashTable         *dict)
+{
+    nm_auto_unref_object NMDeviceGeneric *self     = NM_DEVICE_GENERIC(user_data);
+    NMDeviceGenericPrivate               *priv     = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+    NMDevice                             *device   = NM_DEVICE(self);
+    NMPlatform                           *platform = nm_device_get_platform(device);
+    const NMPlatformLink                 *link;
+    int                                   ifindex = -1;
+    const char                           *ifindex_str;
+    NMSettingConnection                  *s_con;
+
+    nm_assert(call_id == priv->dispatcher_call_id);
+    priv->dispatcher_call_id = NULL;
+
+    if (!success) {
+        _LOGW(LOGD_CORE, "device handler 'device-add' failed: %s", error);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return;
+    }
+
+    ifindex_str = g_hash_table_lookup(dict, "IFINDEX");
+    if (!ifindex_str) {
+        _LOGW(LOGD_CORE, "device handler 'device-add' didn't return a IFINDEX key");
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return;
+    }
+
+    ifindex = _nm_utils_ascii_str_to_int64(ifindex_str, 10, 1, G_MAXINT32, -1);
+    if (ifindex < 0) {
+        _LOGW(LOGD_CORE, "device handler 'device-add' returned invalid ifindex '%s'", ifindex_str);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "device handler 'device-add' returned ifindex %d", ifindex);
+
+    /* Check that the ifindex is valid and matches the interface name. */
+    nm_platform_process_events(platform);
+    link = nm_platform_link_get(platform, ifindex);
+    if (!link) {
+        _LOGW(LOGD_DEVICE,
+              "device handler 'device-add' didn't create link with ifindex %d",
+              ifindex);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return;
+    }
+
+    s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION);
+    nm_assert(s_con);
+
+    if (!nm_streq(link->name, nm_setting_connection_get_interface_name(s_con))) {
+        _LOGW(LOGD_DEVICE,
+              "device handler 'device-add' created a kernel link with name '%s' instead of '%s'",
+              link->name,
+              nm_setting_connection_get_interface_name(s_con));
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return;
+    }
+
+    priv->prepare_done = TRUE;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *self, NMDeviceStateReason *out_failure_reason)
+{
+    NMDevice               *device = NM_DEVICE(self);
+    NMDeviceGenericPrivate *priv   = NM_DEVICE_GENERIC_GET_PRIVATE(device);
+    NMSettingGeneric       *s_generic;
+    const char             *type_desc;
+    int                     ifindex;
+
+    s_generic = nm_device_get_applied_setting(device, NM_TYPE_SETTING_GENERIC);
+    g_return_val_if_fail(s_generic, NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (!nm_setting_generic_get_device_handler(s_generic))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    if (priv->prepare_done) {
+        /* after we create a new interface via a device-handler, update the
+         * type description */
+        ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+        if (ifindex > 0) {
+            type_desc = nm_platform_link_get_type_name(nm_device_get_platform(device), ifindex);
+            if (!nm_streq0(priv->type_description, type_desc)) {
+                priv->type_description = type_desc;
+                _notify(NM_DEVICE_GENERIC(self), PROP_TYPE_DESCRIPTION);
+            }
+        }
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    if (priv->dispatcher_call_id) {
+        nm_dispatcher_call_cancel(priv->dispatcher_call_id);
+        priv->dispatcher_call_id = NULL;
+    }
+
+    _LOGD(LOGD_CORE, "calling device handler 'device-add'");
+    if (!nm_dispatcher_call_device_handler(NM_DISPATCHER_ACTION_DEVICE_ADD,
+                                           device,
+                                           NULL,
+                                           device_add_dispatcher_cb,
+                                           g_object_ref(self),
+                                           &priv->dispatcher_call_id)) {
+        _LOGW(LOGD_DEVICE, "failed to call device handler 'device-add'");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+act_stage3_ip_config(NMDevice *device, int addr_family)
+{
+    nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
 }
 
 static const char *
@@ -110,6 +262,111 @@ update_connection(NMDevice *device, NMConnection *connection)
                  NULL);
 }
 
+static gboolean
+create_and_realize(NMDevice              *device,
+                   NMConnection          *connection,
+                   NMDevice              *parent,
+                   const NMPlatformLink **out_plink,
+                   GError               **error)
+{
+    /* The actual interface is created during stage1 once the device
+     * starts activating, as we need to call the dispatcher service
+     * which returns asynchronously */
+    return TRUE;
+}
+
+static void
+deactivate_clear_data(NMDeviceGeneric *self)
+{
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    if (priv->dispatcher_call_id) {
+        nm_dispatcher_call_cancel(priv->dispatcher_call_id);
+        priv->dispatcher_call_id = NULL;
+    }
+
+    priv->deactivate.callback      = NULL;
+    priv->deactivate.callback_data = NULL;
+    g_clear_object(&priv->deactivate.cancellable);
+}
+
+static void
+device_delete_dispatcher_cb(NMDispatcherCallId *call_id,
+                            gpointer            user_data,
+                            gboolean            success,
+                            const char         *error,
+                            GHashTable         *dict)
+{
+    NMDeviceGeneric        *self  = user_data;
+    NMDeviceGenericPrivate *priv  = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+    gs_free_error GError   *local = NULL;
+
+    nm_assert(call_id == priv->dispatcher_call_id);
+    priv->dispatcher_call_id = NULL;
+
+    if (success)
+        _LOGT(LOGD_DEVICE, "deactivate: async callback");
+    else {
+        local = g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_FAILED,
+                            "device handler 'device-delete' failed with error: %s",
+                            error);
+    }
+
+    priv->deactivate.callback(NM_DEVICE(self), local, priv->deactivate.callback_data);
+    nm_clear_g_cancellable_disconnect(priv->deactivate.cancellable,
+                                      &priv->deactivate.cancellable_id);
+    deactivate_clear_data(self);
+}
+
+static void
+deactivate_cancellable_cancelled(GCancellable *cancellable, NMDeviceGeneric *self)
+{
+    NMDeviceGenericPrivate *priv  = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+    gs_free_error GError   *error = NULL;
+
+    error = nm_utils_error_new_cancelled(FALSE, NULL);
+    priv->deactivate.callback(NM_DEVICE(self), error, priv->deactivate.callback_data);
+
+    deactivate_clear_data(self);
+}
+
+static void
+deactivate_async(NMDevice                  *device,
+                 GCancellable              *cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   callback_user_data)
+{
+    NMDeviceGeneric        *self = NM_DEVICE_GENERIC(device);
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    _LOGT(LOGD_CORE, "deactivate: start async");
+
+    priv->prepare_done = FALSE;
+
+    if (priv->dispatcher_call_id) {
+        nm_dispatcher_call_cancel(priv->dispatcher_call_id);
+        priv->dispatcher_call_id = NULL;
+    }
+
+    g_object_ref(self);
+    priv->deactivate.callback      = callback;
+    priv->deactivate.callback_data = callback_user_data;
+    priv->deactivate.cancellable   = g_object_ref(cancellable);
+    priv->deactivate.cancellable_id =
+        g_cancellable_connect(cancellable,
+                              G_CALLBACK(deactivate_cancellable_cancelled),
+                              self,
+                              NULL);
+
+    nm_dispatcher_call_device_handler(NM_DISPATCHER_ACTION_DEVICE_DELETE,
+                                      device,
+                                      NULL,
+                                      device_delete_dispatcher_cb,
+                                      self,
+                                      &priv->dispatcher_call_id);
+}
+
 /*****************************************************************************/
 
 static void
@@ -122,6 +379,26 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_TYPE_DESCRIPTION:
         g_value_set_string(value, priv->type_description);
         break;
+    case PROP_HAS_DEVICE_HANDLER:
+        g_value_set_boolean(value, priv->has_device_handler);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+    NMDeviceGeneric        *self = (NMDeviceGeneric *) object;
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_HAS_DEVICE_HANDLER:
+        /* construct-only */
+        priv->has_device_handler = g_value_get_boolean(value);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -137,16 +414,41 @@ nm_device_generic_init(NMDeviceGeneric *self)
 static GObject *
 constructor(GType type, guint n_construct_params, GObjectConstructParam *construct_params)
 {
-    GObject *object;
+    GObject                *object;
+    NMDeviceGenericPrivate *priv;
 
     object = G_OBJECT_CLASS(nm_device_generic_parent_class)
                  ->constructor(type, n_construct_params, construct_params);
 
-    nm_device_set_unmanaged_flags((NMDevice *) object, NM_UNMANAGED_BY_DEFAULT, TRUE);
+    priv = NM_DEVICE_GENERIC_GET_PRIVATE(object);
+    /* If the device is software (has a device-handler), don't set
+     * unmanaged-by-default so that the device can autoconnect if
+     * necessary. */
+    if (!priv->has_device_handler)
+        nm_device_set_unmanaged_flags((NMDevice *) object, NM_UNMANAGED_BY_DEFAULT, TRUE);
 
     return object;
 }
 
+static NMDevice *
+create_device(NMDeviceFactory      *factory,
+              const char           *iface,
+              const NMPlatformLink *plink,
+              NMConnection         *connection,
+              gboolean             *out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_GENERIC,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Generic",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_GENERIC,
+                        NM_DEVICE_GENERIC_HAS_DEVICE_HANDLER,
+                        TRUE,
+                        NULL);
+}
+
 NMDevice *
 nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing)
 {
@@ -188,6 +490,7 @@ nm_device_generic_class_init(NMDeviceGenericClass *klass)
 
     object_class->constructor  = constructor;
     object_class->get_property = get_property;
+    object_class->set_property = set_property;
 
     dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_generic);
 
@@ -195,10 +498,14 @@ nm_device_generic_class_init(NMDeviceGenericClass *klass)
     device_class->connection_type_check_compatible = NM_SETTING_GENERIC_SETTING_NAME;
     device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_ANY);
 
-    device_class->realize_start_notify        = realize_start_notify;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage3_ip_config        = act_stage3_ip_config;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->deactivate_async            = deactivate_async;
     device_class->get_generic_capabilities    = get_generic_capabilities;
     device_class->get_type_description        = get_type_description;
-    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->realize_start_notify        = realize_start_notify;
     device_class->update_connection           = update_connection;
 
     obj_properties[PROP_TYPE_DESCRIPTION] =
@@ -207,6 +514,18 @@ nm_device_generic_class_init(NMDeviceGenericClass *klass)
                             "",
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
+    obj_properties[PROP_HAS_DEVICE_HANDLER] = g_param_spec_boolean(
+        NM_DEVICE_GENERIC_HAS_DEVICE_HANDLER,
+        "",
+        "",
+        FALSE,
+        G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 }
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    GENERIC,
+    Generic,
+    generic,
+    NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GENERIC_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-generic.h b/src/core/devices/nm-device-generic.h
index f06a5bdc..07cb5447 100644
--- a/src/core/devices/nm-device-generic.h
+++ b/src/core/devices/nm-device-generic.h
@@ -18,7 +18,8 @@
 #define NM_DEVICE_GENERIC_GET_CLASS(obj) \
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGenericClass))
 
-#define NM_DEVICE_GENERIC_TYPE_DESCRIPTION "type-description"
+#define NM_DEVICE_GENERIC_TYPE_DESCRIPTION   "type-description"
+#define NM_DEVICE_GENERIC_HAS_DEVICE_HANDLER "has-device-handler"
 
 typedef struct _NMDeviceGeneric      NMDeviceGeneric;
 typedef struct _NMDeviceGenericClass NMDeviceGenericClass;
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 130708bb..32fab5be 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -10,6 +10,7 @@
 #include <linux/if_ether.h>
 
 #include "nm-act-request.h"
+#include "nm-config.h"
 #include "nm-device-private.h"
 #include "libnm-platform/nm-platform.h"
 #include "nm-device-factory.h"
@@ -190,6 +191,7 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
     NMConnection                       *connection;
     const char                         *con_uuid;
     guint32                             mtu;
+    int                                 offload;
 
     connection = nm_device_get_applied_connection(NM_DEVICE(self));
 
@@ -205,7 +207,20 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
 
     g_return_val_if_fail(s_macsec, NULL);
 
-    if (!nm_supplicant_config_add_setting_macsec(config, s_macsec, error)) {
+    offload = nm_setting_macsec_get_offload(s_macsec);
+    if (offload == NM_SETTING_MACSEC_OFFLOAD_DEFAULT) {
+        offload = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                              NM_CON_DEFAULT("macsec.offload"),
+                                                              NM_DEVICE(self),
+                                                              NM_SETTING_MACSEC_OFFLOAD_OFF,
+                                                              NM_SETTING_MACSEC_OFFLOAD_MAC,
+                                                              NM_SETTING_MACSEC_OFFLOAD_OFF);
+    }
+
+    if (!nm_supplicant_config_add_setting_macsec(config,
+                                                 s_macsec,
+                                                 (NMSettingMacsecOffload) offload,
+                                                 error)) {
         g_prefix_error(error, "macsec-setting: ");
         return NULL;
     }
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index 2bf24ae6..ed0a2738 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -127,7 +127,9 @@ NM_UTILS_LOOKUP_STR_DEFINE(
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED, "ip-method-unsupported"),
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED,
                              "sriov-configuration-failed"),
-    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND, "peer-not-found"), );
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PEER_NOT_FOUND, "peer-not-found"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DEVICE_HANDLER_FAILED,
+                             "device-handler-failed"), );
 
 NM_UTILS_LOOKUP_STR_DEFINE(nm_device_mtu_source_to_string,
                            NMDeviceMtuSource,
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index a9e8c085..34022efb 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -137,8 +137,7 @@ typedef struct {
     GCancellable           *cancellable;
     NMPlatformAsyncCallback callback;
     gpointer                callback_data;
-    guint                   num_vfs;
-    NMOptionBool            autoprobe;
+    NMPlatformSriovParams   sriov_params;
 } SriovOp;
 
 typedef enum {
@@ -7706,8 +7705,7 @@ sriov_op_start(NMDevice *self, SriovOp *op)
 
     nm_platform_link_set_sriov_params_async(nm_device_get_platform(self),
                                             priv->ifindex,
-                                            op->num_vfs,
-                                            op->autoprobe,
+                                            op->sriov_params,
                                             sriov_op_cb,
                                             op,
                                             op->cancellable);
@@ -7768,11 +7766,14 @@ sriov_op_queue_op(NMDevice *self, SriovOp *op)
 }
 
 static void
-sriov_op_queue(NMDevice               *self,
-               guint                   num_vfs,
-               NMOptionBool            autoprobe,
-               NMPlatformAsyncCallback callback,
-               gpointer                callback_data)
+sriov_op_queue(NMDevice                *self,
+               guint                    num_vfs,
+               NMOptionBool             autoprobe,
+               NMSriovEswitchMode       eswitch_mode,
+               NMSriovEswitchInlineMode eswitch_inline_mode,
+               NMSriovEswitchEncapMode  eswitch_encap_mode,
+               NMPlatformAsyncCallback  callback,
+               gpointer                 callback_data)
 {
     SriovOp *op;
 
@@ -7797,8 +7798,14 @@ sriov_op_queue(NMDevice               *self,
 
     op  = g_slice_new(SriovOp);
     *op = (SriovOp){
-        .num_vfs       = num_vfs,
-        .autoprobe     = autoprobe,
+        .sriov_params =
+            (NMPlatformSriovParams){
+                .num_vfs             = num_vfs,
+                .autoprobe           = autoprobe,
+                .eswitch_mode        = (_NMSriovEswitchMode) eswitch_mode,
+                .eswitch_inline_mode = (_NMSriovEswitchInlineMode) eswitch_inline_mode,
+                .eswitch_encap_mode  = (_NMSriovEswitchEncapMode) eswitch_encap_mode,
+            },
         .callback      = callback,
         .callback_data = callback_data,
     };
@@ -7823,7 +7830,14 @@ device_init_static_sriov_num_vfs(NMDevice *self)
             -1,
             -1);
         if (num_vfs >= 0)
-            sriov_op_queue(self, num_vfs, NM_OPTION_BOOL_DEFAULT, NULL, NULL);
+            sriov_op_queue(self,
+                           num_vfs,
+                           NM_OPTION_BOOL_DEFAULT,
+                           NM_SRIOV_ESWITCH_MODE_PRESERVE,
+                           NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE,
+                           NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE,
+                           NULL,
+                           NULL);
     }
 }
 
@@ -10004,6 +10018,9 @@ activate_stage1_device_prepare(NMDevice *self)
             sriov_op_queue(self,
                            nm_setting_sriov_get_total_vfs(s_sriov),
                            NM_TERNARY_TO_OPTION_BOOL(autoprobe),
+                           nm_setting_sriov_get_eswitch_mode(s_sriov),
+                           nm_setting_sriov_get_eswitch_inline_mode(s_sriov),
+                           nm_setting_sriov_get_eswitch_encap_mode(s_sriov),
                            sriov_params_cb,
                            nm_utils_user_data_pack(self, g_steal_pointer(&plat_vfs)));
             priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_PENDING;
@@ -10880,7 +10897,16 @@ _dev_ipdhcpx_notify(NMDhcpClient *client, const NMDhcpClientNotifyData *notify_d
     case NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE:
 
         if (!notify_data->lease_update.l3cd) {
+            const NML3ConfigData *dhcp_l3cd = priv->l3cds[L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4)].d;
+
             _LOGT_ipdhcp(addr_family, "lease lost");
+            if (dhcp_l3cd
+                && nm_l3cfg_remove_config(
+                    priv->l3cfg,
+                    _dev_l3_config_data_tag_get(priv, L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4)),
+                    dhcp_l3cd)) {
+                _dev_l3_cfg_commit(self, FALSE);
+            }
             goto lease_update_out;
         }
 
@@ -16711,6 +16737,9 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
                 sriov_op_queue(self,
                                0,
                                NM_OPTION_BOOL_TRUE,
+                               NM_SRIOV_ESWITCH_MODE_PRESERVE,
+                               NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE,
+                               NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE,
                                sriov_reset_on_deactivate_cb,
                                nm_utils_user_data_pack(self, GINT_TO_POINTER(reason)));
             }
@@ -16760,7 +16789,14 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
         if (priv->ifindex > 0
             && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) {
             priv->sriov_reset_pending++;
-            sriov_op_queue(self, 0, NM_OPTION_BOOL_TRUE, sriov_reset_on_failure_cb, self);
+            sriov_op_queue(self,
+                           0,
+                           NM_OPTION_BOOL_TRUE,
+                           NM_SRIOV_ESWITCH_MODE_PRESERVE,
+                           NM_SRIOV_ESWITCH_INLINE_MODE_PRESERVE,
+                           NM_SRIOV_ESWITCH_ENCAP_MODE_PRESERVE,
+                           sriov_reset_on_failure_cb,
+                           self);
             break;
         }
         /* Schedule the transition to DISCONNECTED.  The device can't transition
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 4be03f4b..8770656b 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -899,6 +899,12 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
 
     l3_cfg_notify_check_connected(self);
 
+    if (!priv->l3cd_curr) {
+        /* When the lease is lost, any cached ACD information is no longer relevant.
+         * Remove it so that it doesn't interfere with a new lease we might get. */
+        _acd_state_reset(self, TRUE, TRUE);
+    }
+
     _emit_notify(self,
                  NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
                  .lease_update = {
diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c
index c8f7ed0c..e6b1a94e 100644
--- a/src/core/ndisc/nm-ndisc.c
+++ b/src/core/ndisc/nm-ndisc.c
@@ -114,7 +114,7 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     guint32                                 ifa_flags;
     guint                                   i;
-    const gint32                            now_sec = nm_utils_get_monotonic_timestamp_sec();
+    const gint64                            now_msec = nm_utils_get_monotonic_timestamp_msec();
 
     l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_NDISC);
 
@@ -134,12 +134,10 @@ nm_ndisc_data_to_l3cd(NMDedupMultiIndex        *multi_idx,
             .ifindex   = ifindex,
             .address   = ndisc_addr->address,
             .plen      = 64,
-            .timestamp = now_sec,
-            .lifetime  = _nm_ndisc_lifetime_from_expiry(((gint64) now_sec) * 1000,
-                                                       ndisc_addr->expiry_msec,
-                                                       TRUE),
+            .timestamp = now_msec / 1000,
+            .lifetime  = _nm_ndisc_lifetime_from_expiry(now_msec, ndisc_addr->expiry_msec, TRUE),
             .preferred = _nm_ndisc_lifetime_from_expiry(
-                ((gint64) now_sec) * 1000,
+                now_msec,
                 NM_MIN(ndisc_addr->expiry_msec, ndisc_addr->expiry_preferred_msec),
                 TRUE),
             .addr_source = NM_IP_CONFIG_SOURCE_NDISC,
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index 5db4a92a..43eb3646 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -2354,9 +2354,10 @@ _nm_config_state_set(NMConfig *self, gboolean allow_persist, gboolean force_pers
     "route-metric-default-aspired"
 #define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROUTE_METRIC_DEFAULT_EFFECTIVE \
     "route-metric-default-effective"
-#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH     "root-path"
-#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER   "next-server"
-#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_DHCP_BOOTFILE "dhcp-bootfile"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_ROOT_PATH        "root-path"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_NEXT_SERVER      "next-server"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_DHCP_BOOTFILE    "dhcp-bootfile"
+#define DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_GENERIC_SOFTWARE "generic-software"
 
 static NM_UTILS_LOOKUP_STR_DEFINE(
     _device_state_managed_type_to_str,
@@ -2457,6 +2458,12 @@ _config_device_state_data_new(int ifindex, GKeyFile *kf)
     device_state->route_metric_default_aspired   = route_metric_default_aspired;
     device_state->route_metric_default_effective = route_metric_default_effective;
 
+    device_state->generic_sw =
+        nm_config_keyfile_get_boolean(kf,
+                                      DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
+                                      DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_GENERIC_SOFTWARE,
+                                      FALSE);
+
     p = (char *) (&device_state[1]);
     if (connection_uuid) {
         memcpy(p, connection_uuid, connection_uuid_len);
@@ -2502,7 +2509,7 @@ nm_config_device_state_load(int ifindex)
                        ? ", nm-owned=1"
                        : (device_state->nm_owned == NM_TERNARY_FALSE ? ", nm-owned=0" : "");
 
-    _LOGT("device-state: %s #%d (%s); managed=%s%s%s%s%s%s%s%s, "
+    _LOGT("device-state: %s #%d (%s); managed=%s%s%s%s%s%s%s%s%s, "
           "route-metric-default=%" G_GUINT32_FORMAT "-%" G_GUINT32_FORMAT "",
           kf ? "read" : "miss",
           ifindex,
@@ -2519,6 +2526,7 @@ nm_config_device_state_load(int ifindex)
                               "",
                               ""),
           nm_owned_str,
+          device_state->generic_sw ? ", generic-software" : "",
           device_state->route_metric_default_aspired,
           device_state->route_metric_default_effective);
 
@@ -2577,7 +2585,8 @@ nm_config_device_state_write(int                            ifindex,
                              guint32                        route_metric_default_aspired,
                              guint32                        route_metric_default_effective,
                              NMDhcpConfig                  *dhcp4_config,
-                             NMDhcpConfig                  *dhcp6_config)
+                             NMDhcpConfig                  *dhcp6_config,
+                             gboolean                       generic_sw)
 {
     char    path[NM_STRLEN(NM_CONFIG_DEVICE_STATE_DIR "/") + DEVICE_STATE_FILENAME_LEN_MAX + 1];
     GError *local                                 = NULL;
@@ -2664,6 +2673,13 @@ nm_config_device_state_write(int                            ifindex,
                               dhcp_bootfile);
     }
 
+    if (generic_sw) {
+        g_key_file_set_boolean(kf,
+                               DEVICE_RUN_STATE_KEYFILE_GROUP_DEVICE,
+                               DEVICE_RUN_STATE_KEYFILE_KEY_DEVICE_GENERIC_SOFTWARE,
+                               TRUE);
+    }
+
     for (IS_IPv4 = 1; IS_IPv4 >= 0; IS_IPv4--) {
         NMDhcpConfig              *dhcp_config = IS_IPv4 ? dhcp4_config : dhcp6_config;
         gs_free NMUtilsNamedValue *values      = NULL;
@@ -2691,7 +2707,7 @@ nm_config_device_state_write(int                            ifindex,
         g_error_free(local);
         return FALSE;
     }
-    _LOGT("device-state: write #%d (%s); managed=%s%s%s%s%s%s%s, "
+    _LOGT("device-state: write #%d (%s); managed=%s%s%s%s%s%s%s%s, "
           "route-metric-default=%" G_GUINT32_FORMAT "-%" G_GUINT32_FORMAT "%s%s%s"
           "%s%s%s"
           "%s%s%s",
@@ -2700,6 +2716,7 @@ nm_config_device_state_write(int                            ifindex,
           _device_state_managed_type_to_str(managed),
           NM_PRINT_FMT_QUOTED(connection_uuid, ", connection-uuid=", connection_uuid, "", ""),
           NM_PRINT_FMT_QUOTED(perm_hw_addr_fake, ", perm-hw-addr-fake=", perm_hw_addr_fake, "", ""),
+          generic_sw ? ", generic-software" : "",
           route_metric_default_aspired,
           route_metric_default_effective,
           NM_PRINT_FMT_QUOTED(next_server, ", next-server=", next_server, "", ""),
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index acec8d05..e65582c3 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -176,6 +176,8 @@ struct _NMConfigDeviceStateData {
     /* whether the device was nm-owned (0/1) or -1 for
      * non-software devices. */
     NMTernary nm_owned : 3;
+    /* whether the device is a generic one created by NM */
+    bool generic_sw : 1;
 };
 
 NMConfigDeviceStateData *nm_config_device_state_load(int ifindex);
@@ -188,7 +190,8 @@ gboolean                 nm_config_device_state_write(int
                                                       guint32                        route_metric_default_aspired,
                                                       guint32                        route_metric_default_effective,
                                                       NMDhcpConfig                  *dhcp4_config,
-                                                      NMDhcpConfig                  *dhcp6_config);
+                                                      NMDhcpConfig                  *dhcp6_config,
+                                                      gboolean                       generic);
 
 void nm_config_device_state_prune_stale(GHashTable *preserve_ifindexes,
                                         NMPlatform *preserve_in_platform);
diff --git a/src/core/nm-dispatcher.c b/src/core/nm-dispatcher.c
index 9aa4194e..4f442c68 100644
--- a/src/core/nm-dispatcher.c
+++ b/src/core/nm-dispatcher.c
@@ -50,20 +50,24 @@
     }                                                         \
     G_STMT_END
 
-static gboolean nm_dispatcher_need_device(NMDispatcherAction action);
-
 /*****************************************************************************/
 
+/* Type for generic callback function; must be cast to either
+ * NMDispatcherFunc or NMDispatcherFuncDH before using. */
+typedef void (*NMDispatcherCallback)(void);
+
 struct NMDispatcherCallId {
-    NMDispatcherFunc   callback;
-    gpointer           user_data;
-    const char        *log_ifname;
-    const char        *log_con_uuid;
-    gint64             start_at_msec;
-    NMDispatcherAction action;
-    guint              idle_id;
-    guint32            request_id;
-    char               extra_strings[];
+    NMDispatcherCallback callback;
+    gpointer             user_data;
+    const char          *log_ifname;
+    const char          *log_con_uuid;
+    GVariant            *action_params;
+    gint64               start_at_msec;
+    NMDispatcherAction   action;
+    guint                idle_id;
+    guint32              request_id;
+    bool                 is_action2 : 1;
+    char                 extra_strings[];
 };
 
 /*****************************************************************************/
@@ -84,14 +88,34 @@ static struct {
 
 /*****************************************************************************/
 
+/* All actions except 'hostname', 'connectivity-change' and 'dns-change' require
+ * a device */
+static gboolean
+action_need_device(NMDispatcherAction action)
+{
+    if (NM_IN_SET(action,
+                  NM_DISPATCHER_ACTION_HOSTNAME,
+                  NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
+                  NM_DISPATCHER_ACTION_DNS_CHANGE)) {
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+action_is_device_handler(NMDispatcherAction action)
+{
+    return NM_IN_SET(action, NM_DISPATCHER_ACTION_DEVICE_ADD, NM_DISPATCHER_ACTION_DEVICE_DELETE);
+}
+
 static NMDispatcherCallId *
-dispatcher_call_id_new(guint32            request_id,
-                       gint64             start_at_msec,
-                       NMDispatcherAction action,
-                       NMDispatcherFunc   callback,
-                       gpointer           user_data,
-                       const char        *log_ifname,
-                       const char        *log_con_uuid)
+dispatcher_call_id_new(guint32              request_id,
+                       gint64               start_at_msec,
+                       NMDispatcherAction   action,
+                       NMDispatcherCallback callback,
+                       gpointer             user_data,
+                       const char          *log_ifname,
+                       const char          *log_con_uuid)
 {
     NMDispatcherCallId *call_id;
     gsize               l_log_ifname;
@@ -109,6 +133,7 @@ dispatcher_call_id_new(guint32            request_id,
     call_id->callback      = callback;
     call_id->user_data     = user_data;
     call_id->idle_id       = 0;
+    call_id->is_action2    = TRUE;
 
     extra_strings = &call_id->extra_strings[0];
 
@@ -131,6 +156,7 @@ dispatcher_call_id_new(guint32            request_id,
 static void
 dispatcher_call_id_free(NMDispatcherCallId *call_id)
 {
+    nm_clear_pointer(&call_id->action_params, g_variant_unref);
     nm_clear_g_source(&call_id->idle_id);
     g_free(call_id);
 }
@@ -372,20 +398,50 @@ dispatch_result_to_string(DispatchResult result)
     g_assert_not_reached();
 }
 
+/*
+ * dispatcher_results_process:
+ * @action: the dispatcher action
+ * @request_id: request id
+ * @start_at_msec: the timestamp at which the dispatcher call was started
+ * @now_msec: the current timestamp in milliseconds
+ * @log_ifname: the interface name for logging
+ * @log_con_uuid: the connection UUID for logging
+ * @out_success: (out): for device-handler actions, the result of the script
+ * @out_error_msg: (out)(transfer full): for device-handler actions, the
+ *   error message in case of failure
+ * @out_dict: (out)(transfer full): for device-handler actions, the output
+ *   dictionary in case of success
+ * @v_results: the GVariant containing the results to parse
+ * @is_action2: whether the D-Bus method is "Action2()" (or "Action()")
+ *
+ * Process the results of the dispatcher call.
+ *
+ */
 static void
-dispatcher_results_process(guint32     request_id,
-                           gint64      start_at_msec,
-                           gint64      now_msec,
-                           const char *log_ifname,
-                           const char *log_con_uuid,
-                           GVariant   *v_results)
+dispatcher_results_process(NMDispatcherAction action,
+                           guint32            request_id,
+                           gint64             start_at_msec,
+                           gint64             now_msec,
+                           const char        *log_ifname,
+                           const char        *log_con_uuid,
+                           gboolean          *out_success,
+                           char             **out_error_msg,
+                           GHashTable       **out_dict,
+                           GVariant          *v_results,
+                           gboolean           is_action2)
 {
     nm_auto_free_variant_iter GVariantIter *results = NULL;
     const char                             *script, *err;
     guint32                                 result;
     gsize                                   n_children;
+    gboolean                                action_is_dh = action_is_device_handler(action);
 
-    g_variant_get(v_results, "(a(sus))", &results);
+    nm_assert(!action_is_dh || is_action2);
+
+    if (is_action2)
+        g_variant_get(v_results, "(a(susa{sv}))", &results);
+    else
+        g_variant_get(v_results, "(a(sus))", &results);
 
     n_children = g_variant_iter_n_children(results);
 
@@ -397,10 +453,26 @@ dispatcher_results_process(guint32     request_id,
            (int) ((now_msec - start_at_msec) % 1000),
            n_children);
 
-    if (n_children == 0)
+    if (n_children == 0) {
+        if (action_is_dh) {
+            NM_SET_OUT(out_success, FALSE);
+            NM_SET_OUT(out_error_msg, g_strdup("no result returned from dispatcher service"));
+            NM_SET_OUT(out_dict, NULL);
+        }
         return;
+    }
+
+    while (TRUE) {
+        gs_unref_variant GVariant *options = NULL;
+
+        if (is_action2) {
+            if (!g_variant_iter_next(results, "(&su&s@a{sv})", &script, &result, &err, &options))
+                break;
+        } else {
+            if (!g_variant_iter_next(results, "(&su&s)", &script, &result, &err))
+                break;
+        }
 
-    while (g_variant_iter_next(results, "(&su&s)", &script, &result, &err)) {
         if (result == DISPATCH_RESULT_SUCCESS) {
             _LOG2D(request_id, log_ifname, log_con_uuid, "%s succeeded", script);
         } else {
@@ -412,22 +484,96 @@ dispatcher_results_process(guint32     request_id,
                    dispatch_result_to_string(result),
                    err);
         }
+
+        if (action_is_dh) {
+            if (result == DISPATCH_RESULT_SUCCESS) {
+                gs_unref_variant GVariant     *output_dict = NULL;
+                gs_unref_hashtable GHashTable *hash        = NULL;
+                GVariantIter                   iter;
+                const char                    *value;
+                const char                    *key;
+
+                hash = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+                output_dict =
+                    g_variant_lookup_value(options, "output_dict", G_VARIANT_TYPE("a{ss}"));
+                if (output_dict) {
+                    g_variant_iter_init(&iter, output_dict);
+                    while (g_variant_iter_next(&iter, "{&s&s}", &key, &value)) {
+                        const char *unescaped;
+                        gpointer    to_free;
+                        gsize       len;
+
+                        unescaped = nm_utils_buf_utf8safe_unescape(value,
+                                                                   NM_UTILS_STR_UTF8_SAFE_FLAG_NONE,
+                                                                   &len,
+                                                                   &to_free);
+                        g_hash_table_insert(hash,
+                                            g_strdup(key),
+                                            ((char *) to_free) ?: g_strdup(unescaped));
+                    }
+                }
+
+                NM_SET_OUT(out_success, TRUE);
+                NM_SET_OUT(out_dict, g_steal_pointer(&hash));
+                NM_SET_OUT(out_error_msg, NULL);
+            } else {
+                gs_unref_variant GVariant *output_dict = NULL;
+                const char                *err2        = NULL;
+
+                output_dict =
+                    g_variant_lookup_value(options, "output_dict", G_VARIANT_TYPE("a{ss}"));
+                if (output_dict) {
+                    g_variant_lookup(output_dict, "ERROR", "&s", &err2);
+                }
+
+                NM_SET_OUT(out_success, FALSE);
+                NM_SET_OUT(out_dict, NULL);
+                NM_SET_OUT(out_error_msg,
+                           err2 ? g_strdup_printf("%s (Error: %s)", err, err2) : g_strdup(err));
+            }
+            break;
+        }
     }
 }
 
 static void
 dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
-    gs_unref_variant GVariant *ret     = NULL;
-    gs_free_error GError      *error   = NULL;
-    NMDispatcherCallId        *call_id = user_data;
-    gint64                     now_msec;
+    gs_unref_variant GVariant     *ret     = NULL;
+    gs_free_error GError          *error   = NULL;
+    NMDispatcherCallId            *call_id = user_data;
+    gint64                         now_msec;
+    gboolean                       action_is_dh;
+    gboolean                       success   = TRUE;
+    gs_free char                  *error_msg = NULL;
+    gs_unref_hashtable GHashTable *hash      = NULL;
 
     nm_assert((gpointer) source == gl.dbus_connection);
 
     now_msec = nm_utils_get_monotonic_timestamp_msec();
 
     ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+
+    if (!ret && call_id->is_action2 && !action_is_device_handler(call_id->action)
+        && g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
+        _LOG3D(call_id,
+               "dispatcher service does not implement Action2() method, falling back to Action()");
+        call_id->is_action2 = FALSE;
+        g_dbus_connection_call(gl.dbus_connection,
+                               NM_DISPATCHER_DBUS_SERVICE,
+                               NM_DISPATCHER_DBUS_PATH,
+                               NM_DISPATCHER_DBUS_INTERFACE,
+                               "Action",
+                               g_steal_pointer(&call_id->action_params),
+                               G_VARIANT_TYPE("(a(sus))"),
+                               G_DBUS_CALL_FLAGS_NONE,
+                               CALL_TIMEOUT,
+                               NULL,
+                               dispatcher_done_cb,
+                               call_id);
+        return;
+    }
+
     if (!ret) {
         NMLogLevel log_level = LOGL_DEBUG;
 
@@ -442,37 +588,55 @@ dispatcher_done_cb(GObject *source, GAsyncResult *result, gpointer user_data)
                 (int) ((now_msec - call_id->start_at_msec) % 1000),
                 error->message);
     } else {
-        dispatcher_results_process(call_id->request_id,
+        dispatcher_results_process(call_id->action,
+                                   call_id->request_id,
                                    call_id->start_at_msec,
                                    now_msec,
                                    call_id->log_ifname,
                                    call_id->log_con_uuid,
-                                   ret);
+                                   &success,
+                                   &error_msg,
+                                   &hash,
+                                   ret,
+                                   call_id->is_action2);
     }
 
     g_hash_table_remove(gl.requests, call_id);
+    action_is_dh = action_is_device_handler(call_id->action);
+
+    if (call_id->callback) {
+        if (action_is_dh) {
+            NMDispatcherFuncDH cb = (NMDispatcherFuncDH) call_id->callback;
+
+            cb(call_id, call_id->user_data, success, error_msg, hash);
+        } else {
+            NMDispatcherFunc cb = (NMDispatcherFunc) call_id->callback;
 
-    if (call_id->callback)
-        call_id->callback(call_id, call_id->user_data);
+            cb(call_id, call_id->user_data);
+        }
+    }
 
     dispatcher_call_id_free(call_id);
 }
 
-static const char *action_table[] = {[NM_DISPATCHER_ACTION_HOSTNAME]      = NMD_ACTION_HOSTNAME,
-                                     [NM_DISPATCHER_ACTION_PRE_UP]        = NMD_ACTION_PRE_UP,
-                                     [NM_DISPATCHER_ACTION_UP]            = NMD_ACTION_UP,
-                                     [NM_DISPATCHER_ACTION_PRE_DOWN]      = NMD_ACTION_PRE_DOWN,
-                                     [NM_DISPATCHER_ACTION_DOWN]          = NMD_ACTION_DOWN,
-                                     [NM_DISPATCHER_ACTION_VPN_PRE_UP]    = NMD_ACTION_VPN_PRE_UP,
-                                     [NM_DISPATCHER_ACTION_VPN_UP]        = NMD_ACTION_VPN_UP,
-                                     [NM_DISPATCHER_ACTION_VPN_PRE_DOWN]  = NMD_ACTION_VPN_PRE_DOWN,
-                                     [NM_DISPATCHER_ACTION_VPN_DOWN]      = NMD_ACTION_VPN_DOWN,
-                                     [NM_DISPATCHER_ACTION_DHCP_CHANGE_4] = NMD_ACTION_DHCP4_CHANGE,
-                                     [NM_DISPATCHER_ACTION_DHCP_CHANGE_6] = NMD_ACTION_DHCP6_CHANGE,
-                                     [NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] =
-                                         NMD_ACTION_CONNECTIVITY_CHANGE,
-                                     [NM_DISPATCHER_ACTION_REAPPLY]    = NMD_ACTION_REAPPLY,
-                                     [NM_DISPATCHER_ACTION_DNS_CHANGE] = NMD_ACTION_DNS_CHANGE};
+static const char *action_table[] = {
+    [NM_DISPATCHER_ACTION_HOSTNAME]            = NMD_ACTION_HOSTNAME,
+    [NM_DISPATCHER_ACTION_PRE_UP]              = NMD_ACTION_PRE_UP,
+    [NM_DISPATCHER_ACTION_UP]                  = NMD_ACTION_UP,
+    [NM_DISPATCHER_ACTION_PRE_DOWN]            = NMD_ACTION_PRE_DOWN,
+    [NM_DISPATCHER_ACTION_DOWN]                = NMD_ACTION_DOWN,
+    [NM_DISPATCHER_ACTION_VPN_PRE_UP]          = NMD_ACTION_VPN_PRE_UP,
+    [NM_DISPATCHER_ACTION_VPN_UP]              = NMD_ACTION_VPN_UP,
+    [NM_DISPATCHER_ACTION_VPN_PRE_DOWN]        = NMD_ACTION_VPN_PRE_DOWN,
+    [NM_DISPATCHER_ACTION_VPN_DOWN]            = NMD_ACTION_VPN_DOWN,
+    [NM_DISPATCHER_ACTION_DHCP_CHANGE_4]       = NMD_ACTION_DHCP4_CHANGE,
+    [NM_DISPATCHER_ACTION_DHCP_CHANGE_6]       = NMD_ACTION_DHCP6_CHANGE,
+    [NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] = NMD_ACTION_CONNECTIVITY_CHANGE,
+    [NM_DISPATCHER_ACTION_REAPPLY]             = NMD_ACTION_REAPPLY,
+    [NM_DISPATCHER_ACTION_DNS_CHANGE]          = NMD_ACTION_DNS_CHANGE,
+    [NM_DISPATCHER_ACTION_DEVICE_ADD]          = NMD_ACTION_DEVICE_ADD,
+    [NM_DISPATCHER_ACTION_DEVICE_DELETE]       = NMD_ACTION_DEVICE_DELETE,
+};
 
 static const char *
 action_to_string(NMDispatcherAction action)
@@ -482,75 +646,29 @@ action_to_string(NMDispatcherAction action)
     return action_table[(gsize) action];
 }
 
-static gboolean
-_dispatcher_call(NMDispatcherAction    action,
-                 gboolean              blocking,
-                 NMDevice             *device,
-                 NMSettingsConnection *settings_connection,
-                 NMConnection         *applied_connection,
-                 gboolean              activation_type_external,
-                 NMConnectivityState   connectivity_state,
-                 const char           *vpn_iface,
-                 const NML3ConfigData *l3cd,
-                 NMDispatcherFunc      callback,
-                 gpointer              user_data,
-                 NMDispatcherCallId  **out_call_id)
+static GVariant *
+build_call_parameters(NMDispatcherAction    action,
+                      NMDevice             *device,
+                      NMSettingsConnection *settings_connection,
+                      NMConnection         *applied_connection,
+                      gboolean              activation_type_external,
+                      NMConnectivityState   connectivity_state,
+                      const char           *vpn_iface,
+                      const NML3ConfigData *l3cd,
+                      gboolean              is_action2)
 {
+    const char                *connectivity_state_string = "UNKNOWN";
     GVariant                  *connection_dict;
     GVariantBuilder            connection_props;
     GVariantBuilder            device_props;
     GVariantBuilder            device_proxy_props;
     GVariantBuilder            device_ip4_props;
     GVariantBuilder            device_ip6_props;
-    gs_unref_variant GVariant *parameters_floating = NULL;
-    gs_unref_variant GVariant *device_dhcp4_props  = NULL;
-    gs_unref_variant GVariant *device_dhcp6_props  = NULL;
+    gs_unref_variant GVariant *device_dhcp4_props = NULL;
+    gs_unref_variant GVariant *device_dhcp6_props = NULL;
     GVariantBuilder            vpn_proxy_props;
     GVariantBuilder            vpn_ip4_props;
     GVariantBuilder            vpn_ip6_props;
-    NMDispatcherCallId        *call_id;
-    guint                      request_id;
-    const char                *connectivity_state_string = "UNKNOWN";
-    const char                *log_ifname;
-    const char                *log_con_uuid;
-    gint64                     start_at_msec;
-    gint64                     now_msec;
-
-    g_return_val_if_fail(!blocking || (!callback && !user_data), FALSE);
-
-    NM_SET_OUT(out_call_id, NULL);
-
-    _init_dispatcher();
-
-    if (!gl.dbus_connection)
-        return FALSE;
-
-    log_ifname = device ? nm_device_get_iface(device) : NULL;
-    log_con_uuid =
-        settings_connection ? nm_settings_connection_get_uuid(settings_connection) : NULL;
-
-    request_id = ++gl.request_id_counter;
-    if (G_UNLIKELY(!request_id))
-        request_id = ++gl.request_id_counter;
-
-    if (!nm_dispatcher_need_device(action)) {
-        _LOG2D(request_id,
-               log_ifname,
-               log_con_uuid,
-               "dispatching action '%s'%s",
-               action_to_string(action),
-               blocking ? " (blocking)" : (callback ? " (with callback)" : ""));
-    } else {
-        g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
-
-        _LOG2D(request_id,
-               log_ifname,
-               log_con_uuid,
-               "(%s) dispatching action '%s'%s",
-               vpn_iface ?: nm_device_get_iface(device),
-               action_to_string(action),
-               blocking ? " (blocking)" : (callback ? " (with callback)" : ""));
-    }
 
     if (applied_connection)
         connection_dict =
@@ -594,7 +712,7 @@ _dispatcher_call(NMDispatcherAction    action,
     g_variant_builder_init(&vpn_ip6_props, G_VARIANT_TYPE_VARDICT);
 
     /* hostname, DNS and connectivity-change actions don't send device data */
-    if (nm_dispatcher_need_device(action)) {
+    if (action_need_device(action)) {
         fill_device_props(device,
                           &device_props,
                           &device_proxy_props,
@@ -609,25 +727,114 @@ _dispatcher_call(NMDispatcherAction    action,
 
     connectivity_state_string = nm_connectivity_state_to_string(connectivity_state);
 
-    parameters_floating =
-        g_variant_new("(s@a{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}@a{sv}@a{sv}ssa{sv}a{sv}a{sv}b)",
-                      action_to_string(action),
-                      connection_dict,
-                      &connection_props,
-                      &device_props,
-                      &device_proxy_props,
-                      &device_ip4_props,
-                      &device_ip6_props,
-                      device_dhcp4_props ?: nm_g_variant_singleton_aLsvI(),
-                      device_dhcp6_props ?: nm_g_variant_singleton_aLsvI(),
-                      connectivity_state_string,
-                      vpn_iface ?: "",
-                      &vpn_proxy_props,
-                      &vpn_ip4_props,
-                      &vpn_ip6_props,
-                      nm_logging_enabled(LOGL_DEBUG, LOGD_DISPATCH));
-
-    start_at_msec = nm_utils_get_monotonic_timestamp_msec();
+    if (is_action2) {
+        return g_variant_new(
+            "(s@a{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}@a{sv}@a{sv}ssa{sv}a{sv}a{sv}b@a{sv})",
+            action_to_string(action),
+            connection_dict,
+            &connection_props,
+            &device_props,
+            &device_proxy_props,
+            &device_ip4_props,
+            &device_ip6_props,
+            device_dhcp4_props ?: nm_g_variant_singleton_aLsvI(),
+            device_dhcp6_props ?: nm_g_variant_singleton_aLsvI(),
+            connectivity_state_string,
+            vpn_iface ?: "",
+            &vpn_proxy_props,
+            &vpn_ip4_props,
+            &vpn_ip6_props,
+            nm_logging_enabled(LOGL_DEBUG, LOGD_DISPATCH),
+            nm_g_variant_singleton_aLsvI());
+    }
+
+    return g_variant_new("(s@a{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}@a{sv}@a{sv}ssa{sv}a{sv}a{sv}b)",
+                         action_to_string(action),
+                         connection_dict,
+                         &connection_props,
+                         &device_props,
+                         &device_proxy_props,
+                         &device_ip4_props,
+                         &device_ip6_props,
+                         device_dhcp4_props ?: nm_g_variant_singleton_aLsvI(),
+                         device_dhcp6_props ?: nm_g_variant_singleton_aLsvI(),
+                         connectivity_state_string,
+                         vpn_iface ?: "",
+                         &vpn_proxy_props,
+                         &vpn_ip4_props,
+                         &vpn_ip6_props,
+                         nm_logging_enabled(LOGL_DEBUG, LOGD_DISPATCH));
+}
+
+static gboolean
+_dispatcher_call(NMDispatcherAction    action,
+                 gboolean              blocking,
+                 NMDevice             *device,
+                 NMSettingsConnection *settings_connection,
+                 NMConnection         *applied_connection,
+                 gboolean              activation_type_external,
+                 NMConnectivityState   connectivity_state,
+                 const char           *vpn_iface,
+                 const NML3ConfigData *l3cd,
+                 NMDispatcherCallback  callback,
+                 gpointer              user_data,
+                 NMDispatcherCallId  **out_call_id)
+{
+    NMDispatcherCallId        *call_id;
+    guint                      request_id;
+    const char                *log_ifname;
+    const char                *log_con_uuid;
+    gint64                     start_at_msec;
+    gint64                     now_msec;
+    gs_unref_variant GVariant *parameters_floating = NULL;
+    gboolean                   is_action2          = TRUE;
+
+    g_return_val_if_fail(!blocking || (!callback && !user_data), FALSE);
+
+    NM_SET_OUT(out_call_id, NULL);
+
+    _init_dispatcher();
+
+    if (!gl.dbus_connection)
+        return FALSE;
+
+    log_ifname = device ? nm_device_get_iface(device) : NULL;
+    log_con_uuid =
+        settings_connection ? nm_settings_connection_get_uuid(settings_connection) : NULL;
+
+    request_id = ++gl.request_id_counter;
+    if (G_UNLIKELY(!request_id))
+        request_id = ++gl.request_id_counter;
+
+    if (!action_need_device(action)) {
+        _LOG2D(request_id,
+               log_ifname,
+               log_con_uuid,
+               "dispatching action '%s'%s",
+               action_to_string(action),
+               blocking ? " (blocking)" : (callback ? " (with callback)" : ""));
+    } else {
+        g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+
+        _LOG2D(request_id,
+               log_ifname,
+               log_con_uuid,
+               "(%s) dispatching action '%s'%s",
+               vpn_iface ?: nm_device_get_iface(device),
+               action_to_string(action),
+               blocking ? " (blocking)" : (callback ? " (with callback)" : ""));
+    }
+
+    parameters_floating = build_call_parameters(action,
+                                                device,
+                                                settings_connection,
+                                                applied_connection,
+                                                activation_type_external,
+                                                connectivity_state,
+                                                vpn_iface,
+                                                l3cd,
+                                                TRUE);
+    start_at_msec       = nm_utils_get_monotonic_timestamp_msec();
 
     /* Send the action to the dispatcher */
     if (blocking) {
@@ -638,14 +845,44 @@ _dispatcher_call(NMDispatcherAction    action,
                                           NM_DISPATCHER_DBUS_SERVICE,
                                           NM_DISPATCHER_DBUS_PATH,
                                           NM_DISPATCHER_DBUS_INTERFACE,
-                                          "Action",
+                                          "Action2",
                                           g_steal_pointer(&parameters_floating),
-                                          G_VARIANT_TYPE("(a(sus))"),
+                                          G_VARIANT_TYPE("(a(susa{sv}))"),
                                           G_DBUS_CALL_FLAGS_NONE,
                                           CALL_TIMEOUT,
                                           NULL,
                                           &error);
 
+        if (!ret && g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
+            _LOG2D(
+                request_id,
+                log_ifname,
+                log_con_uuid,
+                "dispatcher service does not implement Action2() method, falling back to Action()");
+            g_clear_error(&error);
+            parameters_floating = build_call_parameters(action,
+                                                        device,
+                                                        settings_connection,
+                                                        applied_connection,
+                                                        activation_type_external,
+                                                        connectivity_state,
+                                                        vpn_iface,
+                                                        l3cd,
+                                                        FALSE);
+            ret                 = g_dbus_connection_call_sync(gl.dbus_connection,
+                                              NM_DISPATCHER_DBUS_SERVICE,
+                                              NM_DISPATCHER_DBUS_PATH,
+                                              NM_DISPATCHER_DBUS_INTERFACE,
+                                              "Action",
+                                              g_steal_pointer(&parameters_floating),
+                                              G_VARIANT_TYPE("(a(sus))"),
+                                              G_DBUS_CALL_FLAGS_NONE,
+                                              CALL_TIMEOUT,
+                                              NULL,
+                                              &error);
+            is_action2          = FALSE;
+        }
+
         now_msec = nm_utils_get_monotonic_timestamp_msec();
 
         if (!ret) {
@@ -659,12 +896,17 @@ _dispatcher_call(NMDispatcherAction    action,
                    error->message);
             return FALSE;
         }
-        dispatcher_results_process(request_id,
+        dispatcher_results_process(action,
+                                   request_id,
                                    start_at_msec,
                                    now_msec,
                                    log_ifname,
                                    log_con_uuid,
-                                   ret);
+                                   NULL,
+                                   NULL,
+                                   NULL,
+                                   ret,
+                                   is_action2);
         return TRUE;
     }
 
@@ -676,13 +918,25 @@ _dispatcher_call(NMDispatcherAction    action,
                                      log_ifname,
                                      log_con_uuid);
 
+    /* Since we don't want to cache all the input parameters, already build
+     * and cache the argument for the Action() method in case Action2() fails. */
+    call_id->action_params = build_call_parameters(action,
+                                                   device,
+                                                   settings_connection,
+                                                   applied_connection,
+                                                   activation_type_external,
+                                                   connectivity_state,
+                                                   vpn_iface,
+                                                   l3cd,
+                                                   FALSE);
+
     g_dbus_connection_call(gl.dbus_connection,
                            NM_DISPATCHER_DBUS_SERVICE,
                            NM_DISPATCHER_DBUS_PATH,
                            NM_DISPATCHER_DBUS_INTERFACE,
-                           "Action",
+                           "Action2",
                            g_steal_pointer(&parameters_floating),
-                           G_VARIANT_TYPE("(a(sus))"),
+                           G_VARIANT_TYPE("(a(susa{sv}))"),
                            G_DBUS_CALL_FLAGS_NONE,
                            CALL_TIMEOUT,
                            NULL,
@@ -718,11 +972,45 @@ nm_dispatcher_call_hostname(NMDispatcherFunc     callback,
                             NM_CONNECTIVITY_UNKNOWN,
                             NULL,
                             NULL,
-                            callback,
+                            (NMDispatcherCallback) callback,
                             user_data,
                             out_call_id);
 }
 
+static gboolean
+_dispatcher_call_device(NMDispatcherAction   action,
+                        NMDevice            *device,
+                        gboolean             blocking,
+                        NMActRequest        *act_request,
+                        NMDispatcherCallback callback,
+                        gpointer             user_data,
+                        NMDispatcherCallId **out_call_id)
+{
+    nm_assert(NM_IS_DEVICE(device));
+    if (!act_request) {
+        act_request = nm_device_get_act_request(device);
+        if (!act_request)
+            return FALSE;
+    }
+    nm_assert(NM_IN_SET(nm_active_connection_get_device(NM_ACTIVE_CONNECTION(act_request)),
+                        NULL,
+                        device));
+    return _dispatcher_call(
+        action,
+        blocking,
+        device,
+        nm_act_request_get_settings_connection(act_request),
+        nm_act_request_get_applied_connection(act_request),
+        nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(act_request))
+            == NM_ACTIVATION_TYPE_EXTERNAL,
+        NM_CONNECTIVITY_UNKNOWN,
+        NULL,
+        NULL,
+        callback,
+        user_data,
+        out_call_id);
+}
+
 /**
  * nm_dispatcher_call_device:
  * @action: the %NMDispatcherAction
@@ -747,29 +1035,50 @@ nm_dispatcher_call_device(NMDispatcherAction   action,
                           gpointer             user_data,
                           NMDispatcherCallId **out_call_id)
 {
-    nm_assert(NM_IS_DEVICE(device));
-    if (!act_request) {
-        act_request = nm_device_get_act_request(device);
-        if (!act_request)
-            return FALSE;
-    }
-    nm_assert(NM_IN_SET(nm_active_connection_get_device(NM_ACTIVE_CONNECTION(act_request)),
-                        NULL,
-                        device));
-    return _dispatcher_call(
-        action,
-        FALSE,
-        device,
-        nm_act_request_get_settings_connection(act_request),
-        nm_act_request_get_applied_connection(act_request),
-        nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(act_request))
-            == NM_ACTIVATION_TYPE_EXTERNAL,
-        NM_CONNECTIVITY_UNKNOWN,
-        NULL,
-        NULL,
-        callback,
-        user_data,
-        out_call_id);
+    g_return_val_if_fail(!action_is_device_handler(action), FALSE);
+
+    return _dispatcher_call_device(action,
+                                   device,
+                                   FALSE,
+                                   act_request,
+                                   (NMDispatcherCallback) callback,
+                                   user_data,
+                                   out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_device_handler:
+ * @action: the %NMDispatcherAction, must be device-add or device-remove
+ * @device: the #NMDevice the action applies to
+ * @act_request: the #NMActRequest for the action. If %NULL, use the
+ *   current request of the device.
+ * @callback: a caller-supplied device-handler callback to execute when done
+ * @user_data: caller-supplied pointer passed to @callback
+ * @out_call_id: on success, a call identifier which can be passed to
+ *   nm_dispatcher_call_cancel()
+ *
+ * This method always invokes the device dispatcher action asynchronously.  To ignore
+ * the result, pass %NULL to @callback.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_device_handler(NMDispatcherAction   action,
+                                  NMDevice            *device,
+                                  NMActRequest        *act_request,
+                                  NMDispatcherFuncDH   callback,
+                                  gpointer             user_data,
+                                  NMDispatcherCallId **out_call_id)
+{
+    g_return_val_if_fail(action_is_device_handler(action), FALSE);
+
+    return _dispatcher_call_device(action,
+                                   device,
+                                   FALSE,
+                                   act_request,
+                                   (NMDispatcherCallback) callback,
+                                   user_data,
+                                   out_call_id);
 }
 
 /**
@@ -789,29 +1098,9 @@ nm_dispatcher_call_device_sync(NMDispatcherAction action,
                                NMDevice          *device,
                                NMActRequest      *act_request)
 {
-    nm_assert(NM_IS_DEVICE(device));
-    if (!act_request) {
-        act_request = nm_device_get_act_request(device);
-        if (!act_request)
-            return FALSE;
-    }
-    nm_assert(NM_IN_SET(nm_active_connection_get_device(NM_ACTIVE_CONNECTION(act_request)),
-                        NULL,
-                        device));
-    return _dispatcher_call(
-        action,
-        TRUE,
-        device,
-        nm_act_request_get_settings_connection(act_request),
-        nm_act_request_get_applied_connection(act_request),
-        nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(act_request))
-            == NM_ACTIVATION_TYPE_EXTERNAL,
-        NM_CONNECTIVITY_UNKNOWN,
-        NULL,
-        NULL,
-        NULL,
-        NULL,
-        NULL);
+    g_return_val_if_fail(!action_is_device_handler(action), FALSE);
+
+    return _dispatcher_call_device(action, device, TRUE, act_request, NULL, NULL, NULL);
 }
 
 /**
@@ -852,7 +1141,7 @@ nm_dispatcher_call_vpn(NMDispatcherAction    action,
                             NM_CONNECTIVITY_UNKNOWN,
                             vpn_iface,
                             l3cd,
-                            callback,
+                            (NMDispatcherCallback) callback,
                             user_data,
                             out_call_id);
 }
@@ -879,6 +1168,8 @@ nm_dispatcher_call_vpn_sync(NMDispatcherAction    action,
                             const char           *vpn_iface,
                             const NML3ConfigData *l3cd)
 {
+    g_return_val_if_fail(!action_is_device_handler(action), FALSE);
+
     return _dispatcher_call(action,
                             TRUE,
                             parent_device,
@@ -920,7 +1211,7 @@ nm_dispatcher_call_connectivity(NMConnectivityState  connectivity_state,
                             connectivity_state,
                             NULL,
                             NULL,
-                            callback,
+                            (NMDispatcherCallback) callback,
                             user_data,
                             out_call_id);
 }
@@ -952,7 +1243,10 @@ nm_dispatcher_call_dns_change(void)
 void
 nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
 {
-    if (!call_id || g_hash_table_lookup(gl.requests, call_id) != call_id || !call_id->callback)
+    if (!call_id || g_hash_table_lookup(gl.requests, call_id) != call_id)
+        g_return_if_reached();
+
+    if (!call_id->callback)
         g_return_if_reached();
 
     /* Canceling just means the callback doesn't get called, so set the
@@ -961,16 +1255,3 @@ nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
     _LOG3D(call_id, "cancelling dispatcher callback action");
     call_id->callback = NULL;
 }
-
-/* All actions except 'hostname', 'connectivity-change' and 'dns-change' require
- * a device */
-static gboolean
-nm_dispatcher_need_device(NMDispatcherAction action)
-{
-    if (action == NM_DISPATCHER_ACTION_HOSTNAME
-        || action == NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE
-        || action == NM_DISPATCHER_ACTION_DNS_CHANGE) {
-        return FALSE;
-    }
-    return TRUE;
-}
diff --git a/src/core/nm-dispatcher.h b/src/core/nm-dispatcher.h
index a1cb96b7..2882503b 100644
--- a/src/core/nm-dispatcher.h
+++ b/src/core/nm-dispatcher.h
@@ -24,6 +24,8 @@ typedef enum {
     NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
     NM_DISPATCHER_ACTION_REAPPLY,
     NM_DISPATCHER_ACTION_DNS_CHANGE,
+    NM_DISPATCHER_ACTION_DEVICE_ADD,
+    NM_DISPATCHER_ACTION_DEVICE_DELETE,
 } NMDispatcherAction;
 
 #define NM_DISPATCHER_ACTION_DHCP_CHANGE_X(IS_IPv4) \
@@ -31,7 +33,14 @@ typedef enum {
 
 typedef struct NMDispatcherCallId NMDispatcherCallId;
 
+/* Callback function for regular dispatcher calls */
 typedef void (*NMDispatcherFunc)(NMDispatcherCallId *call_id, gpointer user_data);
+/* Callback function for device-handler dispatcher calls */
+typedef void (*NMDispatcherFuncDH)(NMDispatcherCallId *call_id,
+                                   gpointer            user_data,
+                                   gboolean            success,
+                                   const char         *error_msg,
+                                   GHashTable         *dict);
 
 gboolean nm_dispatcher_call_hostname(NMDispatcherFunc     callback,
                                      gpointer             user_data,
@@ -44,6 +53,13 @@ gboolean nm_dispatcher_call_device(NMDispatcherAction   action,
                                    gpointer             user_data,
                                    NMDispatcherCallId **out_call_id);
 
+gboolean nm_dispatcher_call_device_handler(NMDispatcherAction   action,
+                                           NMDevice            *device,
+                                           NMActRequest        *act_request,
+                                           NMDispatcherFuncDH   callback_dh,
+                                           gpointer             user_data,
+                                           NMDispatcherCallId **out_call_id);
+
 gboolean nm_dispatcher_call_device_sync(NMDispatcherAction action,
                                         NMDevice          *device,
                                         NMActRequest      *act_request);
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 2cf9cb1d..730ba476 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -4168,8 +4168,11 @@ platform_link_added(NMManager                     *self,
         gboolean              compatible = TRUE;
         gs_free_error GError *error      = NULL;
 
-        if (nm_device_get_link_type(candidate) != plink->type)
+        if (nm_device_get_device_type(candidate) == NM_DEVICE_TYPE_GENERIC) {
+            /* generic devices are compatible with all link types */
+        } else if (nm_device_get_link_type(candidate) != plink->type) {
             continue;
+        }
 
         if (!nm_streq(nm_device_get_iface(candidate), plink->name))
             continue;
@@ -4213,8 +4216,12 @@ platform_link_added(NMManager                     *self,
     }
 
 add:
-    /* Try registered device factories */
-    factory = nm_device_factory_manager_find_factory_for_link_type(plink->type);
+    if (dev_state && dev_state->generic_sw) {
+        factory = nm_device_factory_get_generic_factory();
+    } else {
+        /* Try registered device factories */
+        factory = nm_device_factory_manager_find_factory_for_link_type(plink->type);
+    }
     if (factory) {
         gboolean              ignore = FALSE;
         gs_free_error GError *error  = NULL;
@@ -7860,7 +7867,10 @@ nm_manager_write_device_state(NMManager *self, NMDevice *device, int *out_ifinde
                                       route_metric_default_aspired,
                                       route_metric_default_effective,
                                       nm_device_get_dhcp_config(device, AF_INET),
-                                      nm_device_get_dhcp_config(device, AF_INET6)))
+                                      nm_device_get_dhcp_config(device, AF_INET6),
+                                      nm_device_is_software(device)
+                                          && nm_device_get_device_type(device)
+                                                 == NM_DEVICE_TYPE_GENERIC))
         return FALSE;
 
     NM_SET_OUT(out_ifindex, ifindex);
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index 205559ce..1d0bfdbe 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -2263,7 +2263,7 @@ test_software_detect_add(const char *testpath, NMLinkType link_type, int test_mo
 }
 
 /*****************************************************************************/
-
+/*
 static void
 _assert_xgress_qos_mappings_impl(int ifindex, gboolean is_ingress_map, int n_entries, int n, ...)
 {
@@ -2343,7 +2343,8 @@ _assert_vlan_flags(int ifindex, _NMVlanFlags flags)
     g_assert(plnk);
     g_assert_cmpint(plnk->flags, ==, flags);
 }
-
+*/
+/*
 static void
 test_vlan_set_xgress(void)
 {
@@ -2359,7 +2360,6 @@ test_vlan_set_xgress(void)
     ifindex =
         nmtstp_assert_wait_for_link(NM_PLATFORM_GET, DEVICE_NAME, NM_LINK_TYPE_VLAN, 100)->ifindex;
 
-    /* ingress-qos-map */
 
     g_assert(nm_platform_link_vlan_set_ingress_map(NM_PLATFORM_GET, ifindex, 4, 5));
     _assert_ingress_qos_mappings(ifindex, 1, 4, 5);
@@ -2385,14 +2385,12 @@ test_vlan_set_xgress(void)
     g_assert(nm_platform_link_vlan_set_ingress_map(NM_PLATFORM_GET, ifindex, 0, 5));
     _assert_ingress_qos_mappings(ifindex, 3, 0, 5, 3, 8, 4, 5);
 
-    /* Set invalid values: */
     g_assert(nm_platform_link_vlan_set_ingress_map(NM_PLATFORM_GET, ifindex, 8, 3));
     _assert_ingress_qos_mappings(ifindex, 3, 0, 5, 3, 8, 4, 5);
 
     g_assert(nm_platform_link_vlan_set_ingress_map(NM_PLATFORM_GET, ifindex, 9, 4));
     _assert_ingress_qos_mappings(ifindex, 3, 0, 5, 3, 8, 4, 5);
 
-    /* egress-qos-map */
 
     g_assert(nm_platform_link_vlan_set_egress_map(NM_PLATFORM_GET, ifindex, 7, 3));
     _assert_egress_qos_mappings(ifindex, 1, 7, 3);
@@ -2695,7 +2693,7 @@ test_vlan_set_xgress(void)
     nmtstp_link_delete(NULL, -1, ifindex, DEVICE_NAME, TRUE);
     nmtstp_link_delete(NULL, -1, ifindex_parent, PARENT_NAME, TRUE);
 }
-
+*/
 /*****************************************************************************/
 
 static void
@@ -4109,7 +4107,7 @@ _nmtstp_setup_tests(void)
         test_software_detect_add("/link/software/detect/wireguard/1", NM_LINK_TYPE_WIREGUARD, 1);
         test_software_detect_add("/link/software/detect/wireguard/2", NM_LINK_TYPE_WIREGUARD, 2);
 
-        g_test_add_func("/link/software/vlan/set-xgress", test_vlan_set_xgress);
+        //       g_test_add_func("/link/software/vlan/set-xgress", test_vlan_set_xgress);
 
         g_test_add_func("/link/set-properties", test_link_set_properties);
 
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 04e79725..3bcbb71b 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
@@ -1723,7 +1723,7 @@ make_user_setting(shvarFile *ifcfg)
         else
             g_string_set_size(str, 0);
 
-        if (!nms_ifcfg_rh_utils_user_key_decode(key + NM_STRLEN("NM_USER_"), str))
+        if (!nm_utils_env_var_decode_name(key + NM_STRLEN("NM_USER_"), str))
             continue;
 
         if (!s_user)
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
index 50e352d3..b4edefbb 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
@@ -398,115 +398,6 @@ utils_detect_ifcfg_path(const char *path, gboolean only_ifcfg)
     return utils_get_ifcfg_path(path);
 }
 
-void
-nms_ifcfg_rh_utils_user_key_encode(const char *key, GString *str_buffer)
-{
-    gsize i;
-
-    nm_assert(key);
-    nm_assert(str_buffer);
-
-    for (i = 0; key[i]; i++) {
-        char ch = key[i];
-
-        /* we encode the key in only upper case letters, digits, and underscore.
-         * As we expect lower-case letters to be more common, we encode lower-case
-         * letters as upper case, and upper-case letters with a leading underscore. */
-
-        if (ch >= '0' && ch <= '9') {
-            g_string_append_c(str_buffer, ch);
-            continue;
-        }
-        if (ch >= 'a' && ch <= 'z') {
-            g_string_append_c(str_buffer, ch - 'a' + 'A');
-            continue;
-        }
-        if (ch == '.') {
-            g_string_append(str_buffer, "__");
-            continue;
-        }
-        if (ch >= 'A' && ch <= 'Z') {
-            g_string_append_c(str_buffer, '_');
-            g_string_append_c(str_buffer, ch);
-            continue;
-        }
-        g_string_append_printf(str_buffer, "_%03o", (unsigned) ch);
-    }
-}
-
-gboolean
-nms_ifcfg_rh_utils_user_key_decode(const char *name, GString *str_buffer)
-{
-    gsize i;
-
-    nm_assert(name);
-    nm_assert(str_buffer);
-
-    if (!name[0])
-        return FALSE;
-
-    for (i = 0; name[i];) {
-        char ch = name[i];
-
-        if (ch >= '0' && ch <= '9') {
-            g_string_append_c(str_buffer, ch);
-            i++;
-            continue;
-        }
-        if (ch >= 'A' && ch <= 'Z') {
-            g_string_append_c(str_buffer, ch - 'A' + 'a');
-            i++;
-            continue;
-        }
-
-        if (ch == '_') {
-            ch = name[i + 1];
-            if (ch == '_') {
-                g_string_append_c(str_buffer, '.');
-                i += 2;
-                continue;
-            }
-            if (ch >= 'A' && ch <= 'Z') {
-                g_string_append_c(str_buffer, ch);
-                i += 2;
-                continue;
-            }
-            if (ch >= '0' && ch <= '7') {
-                char     ch2, ch3;
-                unsigned v;
-
-                ch2 = name[i + 2];
-                if (!(ch2 >= '0' && ch2 <= '7'))
-                    return FALSE;
-
-                ch3 = name[i + 3];
-                if (!(ch3 >= '0' && ch3 <= '7'))
-                    return FALSE;
-
-#define OCTAL_VALUE(ch) ((unsigned) ((ch) - '0'))
-                v = (OCTAL_VALUE(ch) << 6) + (OCTAL_VALUE(ch2) << 3) + OCTAL_VALUE(ch3);
-                if (v > 0xFF || v == 0)
-                    return FALSE;
-                ch = (char) v;
-                if ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || (ch == '.')
-                    || (ch >= 'a' && ch <= 'z')) {
-                    /* such characters are not expected to be encoded via
-                     * octal representation. The encoding is invalid. */
-                    return FALSE;
-                }
-                g_string_append_c(str_buffer, ch);
-                i += 4;
-                continue;
-            }
-            return FALSE;
-        }
-
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
 /*****************************************************************************/
 
 const char *const _nm_ethtool_ifcfg_names[] = {
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
index 07e5e64d..617c5ef6 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
@@ -2604,7 +2604,7 @@ write_user_setting(NMConnection *connection, shvarFile *ifcfg, GError **error)
 
             g_string_set_size(str, 0);
             g_string_append(str, "NM_USER_");
-            nms_ifcfg_rh_utils_user_key_encode(key, str);
+            nm_utils_env_var_encode_name(key, str);
             svSetValue(ifcfg, str->str, nm_setting_user_get_data(s_user, key));
         }
     }
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 1d9372e0..9ad4a8f9 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -396,14 +396,16 @@ again:
 }
 
 gboolean
-nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
-                                        NMSettingMacsec    *setting,
-                                        GError            **error)
+nm_supplicant_config_add_setting_macsec(NMSupplicantConfig    *self,
+                                        NMSettingMacsec       *setting,
+                                        NMSettingMacsecOffload offload,
+                                        GError               **error)
 {
     const char *value;
     char        buf[32];
     int         port;
     gsize       key_len;
+    const char *offload_str = NULL;
 
     g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
     g_return_val_if_fail(setting != NULL, FALSE);
@@ -472,6 +474,28 @@ nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
             return FALSE;
     }
 
+    switch (offload) {
+    case NM_SETTING_MACSEC_OFFLOAD_OFF:
+        /* This is the default in wpa_supplicant. Don't set the option,
+         * so that if user doesn't enable offload, the connection still
+         * works with previous versions of the supplicant.
+         */
+        break;
+    case NM_SETTING_MACSEC_OFFLOAD_PHY:
+        offload_str = "1";
+        break;
+    case NM_SETTING_MACSEC_OFFLOAD_MAC:
+        offload_str = "2";
+        break;
+    case NM_SETTING_MACSEC_OFFLOAD_DEFAULT:
+        nm_assert_not_reached();
+        break;
+    }
+    if (offload_str
+        && !nm_supplicant_config_add_option(self, "macsec_offload", offload_str, -1, NULL, error)) {
+        return FALSE;
+    }
+
     return TRUE;
 }
 
diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h
index 585cf958..c52b756e 100644
--- a/src/core/supplicant/nm-supplicant-config.h
+++ b/src/core/supplicant/nm-supplicant-config.h
@@ -68,9 +68,10 @@ gboolean nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
                                                 gboolean            wired,
                                                 GError            **error);
 
-gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
-                                                 NMSettingMacsec    *setting,
-                                                 GError            **error);
+gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig    *self,
+                                                 NMSettingMacsec       *setting,
+                                                 NMSettingMacsecOffload offload,
+                                                 GError               **error);
 
 gboolean nm_supplicant_config_enable_pmf_akm(NMSupplicantConfig *self, GError **error);
 
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index 8f2561a6..7842365c 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -87,6 +87,7 @@ static const struct Opt opt_table[] = {
                              "OWE",
                              "NONE", )),
     OPT_INT("macsec_integ_only", 0, 1),
+    OPT_INT("macsec_offload", 0, 2),
     OPT_INT("macsec_policy", 0, 1),
     OPT_INT("macsec_port", 1, 65534),
     OPT_BYTES("mka_cak", 65536),