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.c940
1 files changed, 940 insertions, 0 deletions
diff --git a/src/core/nm-dispatcher.c b/src/core/nm-dispatcher.c
new file mode 100644
index 00000000..0ab409f5
--- /dev/null
+++ b/src/core/nm-dispatcher.c
@@ -0,0 +1,940 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2004 - 2018 Red Hat, Inc.
+ * Copyright (C) 2005 - 2008 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-dispatcher.h"
+
+#include "nm-libnm-core-aux/nm-dispatcher-api.h"
+#include "NetworkManagerUtils.h"
+#include "nm-utils.h"
+#include "nm-connectivity.h"
+#include "nm-act-request.h"
+#include "devices/nm-device.h"
+#include "nm-dhcp-config.h"
+#include "nm-proxy-config.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+#include "nm-manager.h"
+#include "settings/nm-settings-connection.h"
+#include "platform/nm-platform.h"
+#include "nm-core-internal.h"
+
+#define CALL_TIMEOUT (1000 * 60 * 10) /* 10 minutes for all scripts */
+
+#define _NMLOG_DOMAIN      LOGD_DISPATCH
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "dispatcher", __VA_ARGS__)
+
+#define _NMLOG2_DOMAIN LOGD_DISPATCH
+#define _NMLOG2(level, request_id, log_ifname, log_con_uuid, ...)  \
+    nm_log((level),                                                \
+           _NMLOG2_DOMAIN,                                         \
+           (log_ifname),                                           \
+           (log_con_uuid),                                         \
+           "dispatcher: (%u) " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+           (request_id) _NM_UTILS_MACRO_REST(__VA_ARGS__))
+
+#define _NMLOG3_DOMAIN LOGD_DISPATCH
+#define _NMLOG3(level, call_id, ...)                          \
+    G_STMT_START                                              \
+    {                                                         \
+        const NMDispatcherCallId *const _call_id = (call_id); \
+                                                              \
+        _NMLOG2(level,                                        \
+                _call_id->request_id,                         \
+                _call_id->log_ifname,                         \
+                _call_id->log_con_uuid,                       \
+                __VA_ARGS__);                                 \
+    }                                                         \
+    G_STMT_END
+
+/*****************************************************************************/
+
+struct NMDispatcherCallId {
+    NMDispatcherFunc   callback;
+    gpointer           user_data;
+    const char *       log_ifname;
+    const char *       log_con_uuid;
+    NMDispatcherAction action;
+    guint              idle_id;
+    guint32            request_id;
+    char               extra_strings[];
+};
+
+/*****************************************************************************/
+
+/* FIXME(shutdown): on shutdown, we should not run dispatcher scripts synchronously.
+ *   Instead, we should of course still run them asynchronously.
+ *
+ *   Also, we should wait for all pending requests to complete before exiting the main-loop
+ *   (with a watchdog). If we hit a timeout, we log a warning and quit (but leave the scripts
+ *   running).
+ *
+ *   Finally, cleanup the global structures. */
+static struct {
+    GDBusConnection *dbus_connection;
+    GHashTable *     requests;
+    guint            request_id_counter;
+} gl;
+
+/*****************************************************************************/
+
+static NMDispatcherCallId *
+dispatcher_call_id_new(guint32            request_id,
+                       NMDispatcherAction action,
+                       NMDispatcherFunc   callback,
+                       gpointer           user_data,
+                       const char *       log_ifname,
+                       const char *       log_con_uuid)
+{
+    NMDispatcherCallId *call_id;
+    gsize               l_log_ifname;
+    gsize               l_log_con_uuid;
+    char *              extra_strings;
+
+    l_log_ifname   = log_ifname ? (strlen(log_ifname) + 1) : 0u;
+    l_log_con_uuid = log_con_uuid ? (strlen(log_con_uuid) + 1) : 0u;
+
+    call_id = g_malloc(sizeof(NMDispatcherCallId) + l_log_ifname + l_log_con_uuid);
+
+    call_id->action     = action;
+    call_id->request_id = request_id;
+    call_id->callback   = callback;
+    call_id->user_data  = user_data;
+    call_id->idle_id    = 0;
+
+    extra_strings = &call_id->extra_strings[0];
+
+    if (log_ifname) {
+        call_id->log_ifname = extra_strings;
+        memcpy(extra_strings, log_ifname, l_log_ifname);
+        extra_strings += l_log_ifname;
+    } else
+        call_id->log_ifname = NULL;
+
+    if (log_con_uuid) {
+        call_id->log_con_uuid = extra_strings;
+        memcpy(extra_strings, log_con_uuid, l_log_con_uuid);
+    } else
+        call_id->log_con_uuid = NULL;
+
+    return call_id;
+}
+
+static void
+dispatcher_call_id_free(NMDispatcherCallId *call_id)
+{
+    nm_clear_g_source(&call_id->idle_id);
+    g_free(call_id);
+}
+
+/*****************************************************************************/
+
+static void
+_init_dispatcher(void)
+{
+    if (G_UNLIKELY(gl.requests == NULL)) {
+        gl.requests        = g_hash_table_new(nm_direct_hash, NULL);
+        gl.dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);
+
+        if (!gl.dbus_connection)
+            _LOGD("No D-Bus connection to talk with NetworkManager-dispatcher service");
+    }
+}
+
+/*****************************************************************************/
+
+static void
+dump_proxy_to_props(NMProxyConfig *proxy, GVariantBuilder *builder)
+{
+    const char *pac_url = NULL, *pac_script = NULL;
+
+    if (nm_proxy_config_get_method(proxy) == NM_PROXY_CONFIG_METHOD_NONE)
+        return;
+
+    pac_url = nm_proxy_config_get_pac_url(proxy);
+    if (pac_url) {
+        g_variant_builder_add(builder, "{sv}", "pac-url", g_variant_new_string(pac_url));
+    }
+
+    pac_script = nm_proxy_config_get_pac_script(proxy);
+    if (pac_script) {
+        g_variant_builder_add(builder, "{sv}", "pac-script", g_variant_new_string(pac_script));
+    }
+}
+
+static void
+dump_ip_to_props(NMIPConfig *ip, GVariantBuilder *builder)
+{
+    const int        addr_family = nm_ip_config_get_addr_family(ip);
+    const int        IS_IPv4     = NM_IS_IPv4(addr_family);
+    const NMPObject *obj;
+    GVariantBuilder  int_builder;
+    NMDedupMultiIter ipconf_iter;
+    GVariant *       var1;
+    GVariant *       var2;
+    guint            n;
+    guint            i;
+    const NMPObject *default_route;
+
+    if (IS_IPv4)
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("aau"));
+    else
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("a(ayuay)"));
+    default_route = nm_ip_config_best_default_route_get(ip);
+    if (IS_IPv4)
+        nm_ip_config_iter_ip4_address_init(&ipconf_iter, NM_IP4_CONFIG(ip));
+    else
+        nm_ip_config_iter_ip6_address_init(&ipconf_iter, NM_IP6_CONFIG(ip));
+    while (nm_platform_dedup_multi_iter_next_obj(&ipconf_iter,
+                                                 &obj,
+                                                 NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4))) {
+        const NMPlatformIPXAddress *addr = NMP_OBJECT_CAST_IPX_ADDRESS(obj);
+
+        if (IS_IPv4) {
+            guint32   array[3];
+            in_addr_t gw;
+
+            gw = 0u;
+            if (default_route) {
+                gw            = NMP_OBJECT_CAST_IP4_ROUTE(default_route)->gateway;
+                default_route = NULL;
+            }
+            array[0] = addr->a4.address;
+            array[1] = addr->a4.plen;
+            array[2] = gw;
+            g_variant_builder_add(
+                &int_builder,
+                "@au",
+                g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, array, 3, sizeof(guint32)));
+        } else {
+            const struct in6_addr *gw = &in6addr_any;
+
+            if (default_route) {
+                gw            = &NMP_OBJECT_CAST_IP6_ROUTE(default_route)->gateway;
+                default_route = NULL;
+            }
+            var1 = nm_g_variant_new_ay_in6addr(&addr->a6.address);
+            var2 = nm_g_variant_new_ay_in6addr(gw);
+            g_variant_builder_add(&int_builder, "(@ayu@ay)", var1, addr->a6.plen, var2);
+        }
+    }
+    g_variant_builder_add(builder, "{sv}", "addresses", g_variant_builder_end(&int_builder));
+
+    if (IS_IPv4)
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("au"));
+    else
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("aay"));
+    n = nm_ip_config_get_num_nameservers(ip);
+    for (i = 0; i < n; i++) {
+        if (IS_IPv4) {
+            g_variant_builder_add(&int_builder,
+                                  "u",
+                                  nm_ip4_config_get_nameserver(NM_IP4_CONFIG(ip), i));
+        } else {
+            var1 = nm_g_variant_new_ay_in6addr(nm_ip6_config_get_nameserver(NM_IP6_CONFIG(ip), i));
+            g_variant_builder_add(&int_builder, "@ay", var1);
+        }
+    }
+    g_variant_builder_add(builder, "{sv}", "nameservers", g_variant_builder_end(&int_builder));
+
+    g_variant_builder_init(&int_builder, G_VARIANT_TYPE("as"));
+    n = nm_ip_config_get_num_domains(ip);
+    for (i = 0; i < n; i++)
+        g_variant_builder_add(&int_builder, "s", nm_ip_config_get_domain(ip, i));
+    g_variant_builder_add(builder, "{sv}", "domains", g_variant_builder_end(&int_builder));
+
+    if (IS_IPv4) {
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("au"));
+        n = nm_ip4_config_get_num_wins(NM_IP4_CONFIG(ip));
+        for (i = 0; i < n; i++)
+            g_variant_builder_add(&int_builder, "u", nm_ip4_config_get_wins(NM_IP4_CONFIG(ip), i));
+        g_variant_builder_add(builder, "{sv}", "wins-servers", g_variant_builder_end(&int_builder));
+    }
+
+    if (IS_IPv4)
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("aau"));
+    else
+        g_variant_builder_init(&int_builder, G_VARIANT_TYPE("a(ayuayu)"));
+    if (IS_IPv4)
+        nm_ip_config_iter_ip4_route_init(&ipconf_iter, NM_IP4_CONFIG(ip));
+    else
+        nm_ip_config_iter_ip6_route_init(&ipconf_iter, NM_IP6_CONFIG(ip));
+    while (nm_platform_dedup_multi_iter_next_obj(&ipconf_iter,
+                                                 &obj,
+                                                 NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))) {
+        const NMPlatformIPXRoute *route = NMP_OBJECT_CAST_IPX_ROUTE(obj);
+
+        if (NM_PLATFORM_IP_ROUTE_IS_DEFAULT(route))
+            continue;
+
+        if (IS_IPv4) {
+            guint32 array[4];
+
+            array[0] = route->r4.network;
+            array[1] = route->r4.plen;
+            array[2] = route->r4.gateway;
+            array[3] = route->r4.metric;
+            g_variant_builder_add(
+                &int_builder,
+                "@au",
+                g_variant_new_fixed_array(G_VARIANT_TYPE_UINT32, array, 4, sizeof(guint32)));
+        } else {
+            var1 = nm_g_variant_new_ay_in6addr(&route->r6.network);
+            var2 = nm_g_variant_new_ay_in6addr(&route->r6.gateway);
+            g_variant_builder_add(&int_builder,
+                                  "(@ayu@ayu)",
+                                  var1,
+                                  route->r6.plen,
+                                  var2,
+                                  route->r6.metric);
+        }
+    }
+    g_variant_builder_add(builder, "{sv}", "routes", g_variant_builder_end(&int_builder));
+}
+
+static void
+fill_device_props(NMDevice *       device,
+                  GVariantBuilder *dev_builder,
+                  GVariantBuilder *proxy_builder,
+                  GVariantBuilder *ip4_builder,
+                  GVariantBuilder *ip6_builder,
+                  GVariant **      dhcp4_props,
+                  GVariant **      dhcp6_props)
+{
+    NMProxyConfig *proxy_config;
+    NMIP4Config *  ip4_config;
+    NMIP6Config *  ip6_config;
+    NMDhcpConfig * dhcp_config;
+
+    /* If the action is for a VPN, send the VPN's IP interface instead of the device's */
+    g_variant_builder_add(dev_builder,
+                          "{sv}",
+                          NMD_DEVICE_PROPS_IP_INTERFACE,
+                          g_variant_new_string(nm_device_get_ip_iface(device)));
+    g_variant_builder_add(dev_builder,
+                          "{sv}",
+                          NMD_DEVICE_PROPS_INTERFACE,
+                          g_variant_new_string(nm_device_get_iface(device)));
+    g_variant_builder_add(dev_builder,
+                          "{sv}",
+                          NMD_DEVICE_PROPS_TYPE,
+                          g_variant_new_uint32(nm_device_get_device_type(device)));
+    g_variant_builder_add(dev_builder,
+                          "{sv}",
+                          NMD_DEVICE_PROPS_STATE,
+                          g_variant_new_uint32(nm_device_get_state(device)));
+    if (nm_dbus_object_is_exported(NM_DBUS_OBJECT(device))) {
+        g_variant_builder_add(
+            dev_builder,
+            "{sv}",
+            NMD_DEVICE_PROPS_PATH,
+            g_variant_new_object_path(nm_dbus_object_get_path(NM_DBUS_OBJECT(device))));
+    }
+
+    proxy_config = nm_device_get_proxy_config(device);
+    if (proxy_config)
+        dump_proxy_to_props(proxy_config, proxy_builder);
+
+    ip4_config = nm_device_get_ip4_config(device);
+    if (ip4_config)
+        dump_ip_to_props(NM_IP_CONFIG(ip4_config), ip4_builder);
+
+    ip6_config = nm_device_get_ip6_config(device);
+    if (ip6_config)
+        dump_ip_to_props(NM_IP_CONFIG(ip6_config), ip6_builder);
+
+    dhcp_config = nm_device_get_dhcp_config(device, AF_INET);
+    if (dhcp_config)
+        *dhcp4_props = nm_g_variant_ref(nm_dhcp_config_get_options(dhcp_config));
+
+    dhcp_config = nm_device_get_dhcp_config(device, AF_INET6);
+    if (dhcp_config)
+        *dhcp6_props = nm_g_variant_ref(nm_dhcp_config_get_options(dhcp_config));
+}
+
+static void
+fill_vpn_props(NMProxyConfig *  proxy_config,
+               NMIP4Config *    ip4_config,
+               NMIP6Config *    ip6_config,
+               GVariantBuilder *proxy_builder,
+               GVariantBuilder *ip4_builder,
+               GVariantBuilder *ip6_builder)
+{
+    if (proxy_config)
+        dump_proxy_to_props(proxy_config, proxy_builder);
+    if (ip4_config)
+        dump_ip_to_props(NM_IP_CONFIG(ip4_config), ip4_builder);
+    if (ip6_config)
+        dump_ip_to_props(NM_IP_CONFIG(ip6_config), ip6_builder);
+}
+
+static const char *
+dispatch_result_to_string(DispatchResult result)
+{
+    switch (result) {
+    case DISPATCH_RESULT_UNKNOWN:
+        return "unknown";
+    case DISPATCH_RESULT_SUCCESS:
+        return "success";
+    case DISPATCH_RESULT_EXEC_FAILED:
+        return "exec failed";
+    case DISPATCH_RESULT_FAILED:
+        return "failed";
+    case DISPATCH_RESULT_TIMEOUT:
+        return "timed out";
+    }
+    g_assert_not_reached();
+}
+
+static void
+dispatcher_results_process(guint32     request_id,
+                           const char *log_ifname,
+                           const char *log_con_uuid,
+                           GVariant *  v_results)
+{
+    nm_auto_free_variant_iter GVariantIter *results = NULL;
+    const char *                            script, *err;
+    guint32                                 result;
+
+    g_variant_get(v_results, "(a(sus))", &results);
+
+    if (g_variant_iter_n_children(results) == 0) {
+        _LOG2D(request_id, log_ifname, log_con_uuid, "succeeded but no scripts invoked");
+        return;
+    }
+
+    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 {
+            _LOG2W(request_id,
+                   log_ifname,
+                   log_con_uuid,
+                   "%s failed (%s): %s",
+                   script,
+                   dispatch_result_to_string(result),
+                   err);
+        }
+    }
+}
+
+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;
+
+    nm_assert((gpointer) source == gl.dbus_connection);
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (!ret) {
+        if (_nm_dbus_error_has_name(error, "org.freedesktop.systemd1.LoadFailed")) {
+            g_dbus_error_strip_remote_error(error);
+            _LOG3W(call_id, "failed to call dispatcher scripts: %s", error->message);
+        } else {
+            _LOG3D(call_id, "failed to call dispatcher scripts: %s", error->message);
+        }
+    } else {
+        dispatcher_results_process(call_id->request_id,
+                                   call_id->log_ifname,
+                                   call_id->log_con_uuid,
+                                   ret);
+    }
+
+    g_hash_table_remove(gl.requests, call_id);
+
+    if (call_id->callback)
+        call_id->callback(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_DHCP4_CHANGE] = NMD_ACTION_DHCP4_CHANGE,
+                                     [NM_DISPATCHER_ACTION_DHCP6_CHANGE] = NMD_ACTION_DHCP6_CHANGE,
+                                     [NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] =
+                                         NMD_ACTION_CONNECTIVITY_CHANGE};
+
+static const char *
+action_to_string(NMDispatcherAction action)
+{
+    if (G_UNLIKELY((gsize) action >= G_N_ELEMENTS(action_table)))
+        g_return_val_if_reached(NULL);
+    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,
+                 NMProxyConfig *       vpn_proxy_config,
+                 NMIP4Config *         vpn_ip4_config,
+                 NMIP6Config *         vpn_ip6_config,
+                 NMDispatcherFunc      callback,
+                 gpointer              user_data,
+                 NMDispatcherCallId ** out_call_id)
+{
+    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;
+    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;
+
+    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;
+
+    /* All actions except 'hostname' and 'connectivity-change' require a device */
+    if (action == NM_DISPATCHER_ACTION_HOSTNAME
+        || action == NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE) {
+        _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 =
+            nm_connection_to_dbus(applied_connection, NM_CONNECTION_SERIALIZE_NO_SECRETS);
+    else
+        connection_dict = g_variant_new_array(G_VARIANT_TYPE("{sa{sv}}"), NULL, 0);
+
+    g_variant_builder_init(&connection_props, G_VARIANT_TYPE_VARDICT);
+    if (settings_connection) {
+        const char *connection_path;
+        const char *filename;
+
+        connection_path = nm_dbus_object_get_path(NM_DBUS_OBJECT(settings_connection));
+        if (connection_path) {
+            g_variant_builder_add(&connection_props,
+                                  "{sv}",
+                                  NMD_CONNECTION_PROPS_PATH,
+                                  g_variant_new_object_path(connection_path));
+        }
+        filename = nm_settings_connection_get_filename(settings_connection);
+        if (filename) {
+            g_variant_builder_add(&connection_props,
+                                  "{sv}",
+                                  NMD_CONNECTION_PROPS_FILENAME,
+                                  g_variant_new_string(filename));
+        }
+        if (activation_type_external) {
+            g_variant_builder_add(&connection_props,
+                                  "{sv}",
+                                  NMD_CONNECTION_PROPS_EXTERNAL,
+                                  g_variant_new_boolean(TRUE));
+        }
+    }
+
+    g_variant_builder_init(&device_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&device_proxy_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&device_ip4_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&device_ip6_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&vpn_proxy_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&vpn_ip4_props, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_init(&vpn_ip6_props, G_VARIANT_TYPE_VARDICT);
+
+    /* hostname and connectivity-change actions don't send device data */
+    if (action != NM_DISPATCHER_ACTION_HOSTNAME
+        && action != NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE) {
+        fill_device_props(device,
+                          &device_props,
+                          &device_proxy_props,
+                          &device_ip4_props,
+                          &device_ip6_props,
+                          &device_dhcp4_props,
+                          &device_dhcp6_props);
+        if (vpn_ip4_config || vpn_ip6_config) {
+            fill_vpn_props(vpn_proxy_config,
+                           vpn_ip4_config,
+                           vpn_ip6_config,
+                           &vpn_proxy_props,
+                           &vpn_ip4_props,
+                           &vpn_ip6_props);
+        }
+    }
+
+    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 ?: g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0),
+                      device_dhcp6_props ?: g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0),
+                      connectivity_state_string,
+                      vpn_iface ?: "",
+                      &vpn_proxy_props,
+                      &vpn_ip4_props,
+                      &vpn_ip6_props,
+                      nm_logging_enabled(LOGL_DEBUG, LOGD_DISPATCH));
+
+    /* Send the action to the dispatcher */
+    if (blocking) {
+        gs_unref_variant GVariant *ret = NULL;
+        gs_free_error GError *error    = NULL;
+
+        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);
+        if (!ret) {
+            g_dbus_error_strip_remote_error(error);
+            _LOG2W(request_id, log_ifname, log_con_uuid, "failed: %s", error->message);
+            return FALSE;
+        }
+        dispatcher_results_process(request_id, log_ifname, log_con_uuid, ret);
+        return TRUE;
+    }
+
+    call_id =
+        dispatcher_call_id_new(request_id, action, callback, user_data, log_ifname, log_con_uuid);
+
+    g_dbus_connection_call(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,
+                           dispatcher_done_cb,
+                           call_id);
+    g_hash_table_add(gl.requests, call_id);
+    NM_SET_OUT(out_call_id, call_id);
+    return TRUE;
+}
+
+/**
+ * nm_dispatcher_call_hostname:
+ * @callback: a caller-supplied 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 dispatcher action asynchronously.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_hostname(NMDispatcherFunc     callback,
+                            gpointer             user_data,
+                            NMDispatcherCallId **out_call_id)
+{
+    return _dispatcher_call(NM_DISPATCHER_ACTION_HOSTNAME,
+                            FALSE,
+                            NULL,
+                            NULL,
+                            NULL,
+                            FALSE,
+                            NM_CONNECTIVITY_UNKNOWN,
+                            NULL,
+                            NULL,
+                            NULL,
+                            NULL,
+                            callback,
+                            user_data,
+                            out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_device:
+ * @action: the %NMDispatcherAction
+ * @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 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(NMDispatcherAction   action,
+                          NMDevice *           device,
+                          NMActRequest *       act_request,
+                          NMDispatcherFunc     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,
+        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,
+        NULL,
+        NULL,
+        callback,
+        user_data,
+        out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_device_sync():
+ * @action: the %NMDispatcherAction
+ * @device: the #NMDevice the action applies to
+ * @act_request: the #NMActRequest for the action. If %NULL, use the
+ *   current request of the device.
+ *
+ * This method always invokes the dispatcher action synchronously and it may
+ * take a long time to return.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+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,
+        NULL,
+        NULL);
+}
+
+/**
+ * nm_dispatcher_call_vpn():
+ * @action: the %NMDispatcherAction
+ * @settings_connection: the #NMSettingsConnection the action applies to
+ * @applied_connection: the currently applied connection
+ * @parent_device: the parent #NMDevice of the VPN connection
+ * @vpn_iface: the IP interface of the VPN tunnel, if any
+ * @vpn_proxy_config: the #NMProxyConfig of the VPN connection
+ * @vpn_ip4_config: the #NMIP4Config of the VPN connection
+ * @vpn_ip6_config: the #NMIP6Config of the VPN connection
+ * @callback: a caller-supplied 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 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_vpn(NMDispatcherAction    action,
+                       NMSettingsConnection *settings_connection,
+                       NMConnection *        applied_connection,
+                       NMDevice *            parent_device,
+                       const char *          vpn_iface,
+                       NMProxyConfig *       vpn_proxy_config,
+                       NMIP4Config *         vpn_ip4_config,
+                       NMIP6Config *         vpn_ip6_config,
+                       NMDispatcherFunc      callback,
+                       gpointer              user_data,
+                       NMDispatcherCallId ** out_call_id)
+{
+    return _dispatcher_call(action,
+                            FALSE,
+                            parent_device,
+                            settings_connection,
+                            applied_connection,
+                            FALSE,
+                            NM_CONNECTIVITY_UNKNOWN,
+                            vpn_iface,
+                            vpn_proxy_config,
+                            vpn_ip4_config,
+                            vpn_ip6_config,
+                            callback,
+                            user_data,
+                            out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_vpn_sync():
+ * @action: the %NMDispatcherAction
+ * @settings_connection: the #NMSettingsConnection the action applies to
+ * @applied_connection: the currently applied connection
+ * @parent_device: the parent #NMDevice of the VPN connection
+ * @vpn_iface: the IP interface of the VPN tunnel, if any
+ * @vpn_proxy_config: the #NMProxyConfig of the VPN connection
+ * @vpn_ip4_config: the #NMIP4Config of the VPN connection
+ * @vpn_ip6_config: the #NMIP6Config of the VPN connection
+ *
+ * This method always invokes the dispatcher action synchronously and it may
+ * take a long time to return.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_vpn_sync(NMDispatcherAction    action,
+                            NMSettingsConnection *settings_connection,
+                            NMConnection *        applied_connection,
+                            NMDevice *            parent_device,
+                            const char *          vpn_iface,
+                            NMProxyConfig *       vpn_proxy_config,
+                            NMIP4Config *         vpn_ip4_config,
+                            NMIP6Config *         vpn_ip6_config)
+{
+    return _dispatcher_call(action,
+                            TRUE,
+                            parent_device,
+                            settings_connection,
+                            applied_connection,
+                            FALSE,
+                            NM_CONNECTIVITY_UNKNOWN,
+                            vpn_iface,
+                            vpn_proxy_config,
+                            vpn_ip4_config,
+                            vpn_ip6_config,
+                            NULL,
+                            NULL,
+                            NULL);
+}
+
+/**
+ * nm_dispatcher_call_connectivity():
+ * @connectivity_state: the #NMConnectivityState value
+ * @callback: a caller-supplied 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 does not block the caller.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_connectivity(NMConnectivityState  connectivity_state,
+                                NMDispatcherFunc     callback,
+                                gpointer             user_data,
+                                NMDispatcherCallId **out_call_id)
+{
+    return _dispatcher_call(NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
+                            FALSE,
+                            NULL,
+                            NULL,
+                            NULL,
+                            FALSE,
+                            connectivity_state,
+                            NULL,
+                            NULL,
+                            NULL,
+                            NULL,
+                            callback,
+                            user_data,
+                            out_call_id);
+}
+
+void
+nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
+{
+    if (!call_id || g_hash_table_lookup(gl.requests, call_id) != call_id || !call_id->callback)
+        g_return_if_reached();
+
+    /* Canceling just means the callback doesn't get called, so set the
+     * DispatcherInfo's callback to NULL.
+     */
+    _LOG3D(call_id, "cancelling dispatcher callback action");
+    call_id->callback = NULL;
+}