summary refs log tree commit diff
path: root/src/core/nm-dispatcher.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/nm-dispatcher.c')
-rw-r--r--src/core/nm-dispatcher.c677
1 files changed, 479 insertions, 198 deletions
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;
-}