summary refs log tree commit diff
path: root/src/core/devices
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/devices')
-rw-r--r--src/core/devices/adsl/meson.build26
-rw-r--r--src/core/devices/adsl/nm-atm-manager.c271
-rw-r--r--src/core/devices/adsl/nm-device-adsl.c720
-rw-r--r--src/core/devices/adsl/nm-device-adsl.h30
-rw-r--r--src/core/devices/bluetooth/meson.build61
-rw-r--r--src/core/devices/bluetooth/nm-bluez-common.h24
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.c2896
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.h42
-rw-r--r--src/core/devices/bluetooth/nm-bluez5-dun.c857
-rw-r--r--src/core/devices/bluetooth/nm-bluez5-dun.h37
-rw-r--r--src/core/devices/bluetooth/nm-bt-error.c10
-rw-r--r--src/core/devices/bluetooth/nm-bt-error.h19
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.c1412
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.h59
-rw-r--r--src/core/devices/bluetooth/tests/nm-bt-test.c223
-rw-r--r--src/core/devices/meson.build24
-rw-r--r--src/core/devices/nm-acd-manager.c496
-rw-r--r--src/core/devices/nm-acd-manager.h34
-rw-r--r--src/core/devices/nm-device-6lowpan.c339
-rw-r--r--src/core/devices/nm-device-6lowpan.h26
-rw-r--r--src/core/devices/nm-device-bond.c651
-rw-r--r--src/core/devices/nm-device-bond.h25
-rw-r--r--src/core/devices/nm-device-bridge.c1246
-rw-r--r--src/core/devices/nm-device-bridge.h30
-rw-r--r--src/core/devices/nm-device-dummy.c183
-rw-r--r--src/core/devices/nm-device-dummy.h26
-rw-r--r--src/core/devices/nm-device-ethernet-utils.c26
-rw-r--r--src/core/devices/nm-device-ethernet-utils.h11
-rw-r--r--src/core/devices/nm-device-ethernet.c2128
-rw-r--r--src/core/devices/nm-device-ethernet.h39
-rw-r--r--src/core/devices/nm-device-factory.c413
-rw-r--r--src/core/devices/nm-device-factory.h237
-rw-r--r--src/core/devices/nm-device-generic.c238
-rw-r--r--src/core/devices/nm-device-generic.h30
-rw-r--r--src/core/devices/nm-device-infiniband.c504
-rw-r--r--src/core/devices/nm-device-infiniband.h27
-rw-r--r--src/core/devices/nm-device-ip-tunnel.c1299
-rw-r--r--src/core/devices/nm-device-ip-tunnel.h40
-rw-r--r--src/core/devices/nm-device-logging.h52
-rw-r--r--src/core/devices/nm-device-macsec.c1090
-rw-r--r--src/core/devices/nm-device-macsec.h39
-rw-r--r--src/core/devices/nm-device-macvlan.c665
-rw-r--r--src/core/devices/nm-device-macvlan.h30
-rw-r--r--src/core/devices/nm-device-ppp.c375
-rw-r--r--src/core/devices/nm-device-ppp.h23
-rw-r--r--src/core/devices/nm-device-private.h213
-rw-r--r--src/core/devices/nm-device-tun.c571
-rw-r--r--src/core/devices/nm-device-tun.h32
-rw-r--r--src/core/devices/nm-device-veth.c232
-rw-r--r--src/core/devices/nm-device-veth.h27
-rw-r--r--src/core/devices/nm-device-vlan.c688
-rw-r--r--src/core/devices/nm-device-vlan.h33
-rw-r--r--src/core/devices/nm-device-vrf.c375
-rw-r--r--src/core/devices/nm-device-vrf.h24
-rw-r--r--src/core/devices/nm-device-vxlan.c813
-rw-r--r--src/core/devices/nm-device-vxlan.h42
-rw-r--r--src/core/devices/nm-device-wireguard.c2093
-rw-r--r--src/core/devices/nm-device-wireguard.h31
-rw-r--r--src/core/devices/nm-device-wpan.c257
-rw-r--r--src/core/devices/nm-device-wpan.h23
-rw-r--r--src/core/devices/nm-device.c19022
-rw-r--r--src/core/devices/nm-device.h874
-rw-r--r--src/core/devices/nm-lldp-listener.c1122
-rw-r--r--src/core/devices/nm-lldp-listener.h33
-rw-r--r--src/core/devices/ovs/meson.build32
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c164
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.h30
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.c445
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.h26
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c196
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.h25
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c324
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c2658
-rw-r--r--src/core/devices/ovs/nm-ovsdb.h61
-rw-r--r--src/core/devices/team/meson.build30
-rw-r--r--src/core/devices/team/nm-device-team.c1096
-rw-r--r--src/core/devices/team/nm-device-team.h30
-rw-r--r--src/core/devices/team/nm-team-factory.c77
-rw-r--r--src/core/devices/tests/meson.build22
-rw-r--r--src/core/devices/tests/test-acd.c259
-rw-r--r--src/core/devices/tests/test-lldp.c1316
-rw-r--r--src/core/devices/wifi/meson.build75
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c3508
-rw-r--r--src/core/devices/wifi/nm-device-iwd.h49
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.c551
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.h38
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c1280
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.h38
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c3879
-rw-r--r--src/core/devices/wifi/nm-device-wifi.h52
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c1359
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.h53
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.c1051
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.h96
-rw-r--r--src/core/devices/wifi/nm-wifi-common.c176
-rw-r--r--src/core/devices/wifi/nm-wifi-common.h22
-rw-r--r--src/core/devices/wifi/nm-wifi-factory.c157
-rw-r--r--src/core/devices/wifi/nm-wifi-p2p-peer.c699
-rw-r--r--src/core/devices/wifi/nm-wifi-p2p-peer.h91
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.c945
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.h39
-rw-r--r--src/core/devices/wifi/tests/test-devices-wifi.c1609
-rw-r--r--src/core/devices/wwan/libnm-wwan.ver41
-rw-r--r--src/core/devices/wwan/meson.build88
-rw-r--r--src/core/devices/wwan/nm-device-modem.c957
-rw-r--r--src/core/devices/wwan/nm-device-modem.h36
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c1625
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.h29
-rw-r--r--src/core/devices/wwan/nm-modem-manager.c876
-rw-r--r--src/core/devices/wwan/nm-modem-manager.h45
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.c1239
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.h35
-rw-r--r--src/core/devices/wwan/nm-modem.c2037
-rw-r--r--src/core/devices/wwan/nm-modem.h269
-rw-r--r--src/core/devices/wwan/nm-service-providers.c455
-rw-r--r--src/core/devices/wwan/nm-service-providers.h24
-rw-r--r--src/core/devices/wwan/nm-wwan-factory.c146
-rw-r--r--src/core/devices/wwan/tests/test-service-providers.c130
-rw-r--r--src/core/devices/wwan/tests/test-service-providers.xml73
119 files changed, 74201 insertions, 0 deletions
diff --git a/src/core/devices/adsl/meson.build b/src/core/devices/adsl/meson.build
new file mode 100644
index 00000000..95f61d95
--- /dev/null
+++ b/src/core/devices/adsl/meson.build
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_device_plugin_adsl = shared_module(
+  'nm-device-plugin-adsl',
+  sources: files(
+    'nm-atm-manager.c',
+    'nm-device-adsl.c',
+  ),
+  dependencies: core_plugin_dep,
+  c_args: daemon_c_flags,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_adsl
+
+test(
+  'check-local-devices-adsl',
+  check_exports,
+  args: [
+    libnm_device_plugin_adsl.full_path(),
+    linker_script_devices,
+  ],
+)
diff --git a/src/core/devices/adsl/nm-atm-manager.c b/src/core/devices/adsl/nm-atm-manager.c
new file mode 100644
index 00000000..9be9b5ce
--- /dev/null
+++ b/src/core/devices/adsl/nm-atm-manager.c
@@ -0,0 +1,271 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2013 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <gmodule.h>
+#include <libudev.h>
+
+#include "nm-setting-adsl.h"
+#include "nm-device-adsl.h"
+#include "devices/nm-device-factory.h"
+#include "platform/nm-platform.h"
+#include "nm-udev-aux/nm-udev-utils.h"
+
+/*****************************************************************************/
+
+#define NM_TYPE_ATM_MANAGER (nm_atm_manager_get_type())
+#define NM_ATM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ATM_MANAGER, NMAtmManager))
+#define NM_ATM_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_ATM_MANAGER, NMAtmManagerClass))
+#define NM_IS_ATM_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_ATM_MANAGER))
+#define NM_IS_ATM_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_ATM_MANAGER))
+#define NM_ATM_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_ATM_MANAGER, NMAtmManagerClass))
+
+typedef struct {
+    NMUdevClient *udev_client;
+    GSList *      devices;
+} NMAtmManagerPrivate;
+
+typedef struct {
+    NMDeviceFactory     parent;
+    NMAtmManagerPrivate _priv;
+} NMAtmManager;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMAtmManagerClass;
+
+static GType nm_atm_manager_get_type(void);
+
+G_DEFINE_TYPE(NMAtmManager, nm_atm_manager, NM_TYPE_DEVICE_FACTORY);
+
+#define NM_ATM_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMAtmManager, NM_IS_ATM_MANAGER)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(
+    NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_ADSL_SETTING_NAME));
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_ATM_MANAGER, NULL);
+}
+
+/*****************************************************************************/
+
+static gboolean
+dev_get_attrs(struct udev_device *udev_device, const char **out_path, char **out_driver)
+{
+    struct udev_device *parent = NULL;
+    const char *        driver, *path;
+
+    g_return_val_if_fail(udev_device != NULL, FALSE);
+    g_return_val_if_fail(out_path != NULL, FALSE);
+    g_return_val_if_fail(out_driver != NULL, FALSE);
+
+    path = udev_device_get_syspath(udev_device);
+    if (!path) {
+        nm_log_warn(LOGD_PLATFORM, "couldn't determine device path; ignoring...");
+        return FALSE;
+    }
+
+    driver = udev_device_get_driver(udev_device);
+    if (!driver) {
+        /* Try the parent */
+        parent = udev_device_get_parent(udev_device);
+        if (parent)
+            driver = udev_device_get_driver(parent);
+    }
+
+    *out_path   = path;
+    *out_driver = g_strdup(driver);
+
+    return TRUE;
+}
+
+static void
+device_destroyed(gpointer user_data, GObject *dead)
+{
+    NMAtmManager *       self = NM_ATM_MANAGER(user_data);
+    NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self);
+
+    priv->devices = g_slist_remove(priv->devices, dead);
+}
+
+static void
+adsl_add(NMAtmManager *self, struct udev_device *udev_device)
+{
+    NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self);
+    const char *         ifname, *sysfs_path = NULL;
+    char *               driver         = NULL;
+    gs_free char *       atm_index_path = NULL;
+    int                  atm_index;
+    NMDevice *           device;
+
+    g_return_if_fail(udev_device != NULL);
+
+    ifname = udev_device_get_sysname(udev_device);
+    if (!ifname) {
+        nm_log_warn(LOGD_PLATFORM, "failed to get device's interface name");
+        return;
+    }
+
+    nm_log_dbg(LOGD_PLATFORM, "(%s): found ATM device", ifname);
+
+    atm_index_path =
+        g_strdup_printf("/sys/class/atm/%s/atmindex", NM_ASSERT_VALID_PATH_COMPONENT(ifname));
+    atm_index = (int) nm_platform_sysctl_get_int_checked(NM_PLATFORM_GET,
+                                                         NMP_SYSCTL_PATHID_ABSOLUTE(atm_index_path),
+                                                         10,
+                                                         0,
+                                                         G_MAXINT,
+                                                         -1);
+    if (atm_index < 0) {
+        nm_log_warn(LOGD_PLATFORM, "(%s): failed to get ATM index", ifname);
+        return;
+    }
+
+    if (!dev_get_attrs(udev_device, &sysfs_path, &driver)) {
+        nm_log_warn(LOGD_PLATFORM, "(%s): failed to get ATM attributes", ifname);
+        return;
+    }
+
+    g_assert(sysfs_path);
+
+    device = nm_device_adsl_new(sysfs_path, ifname, driver, atm_index);
+    g_assert(device);
+
+    priv->devices = g_slist_prepend(priv->devices, device);
+    g_object_weak_ref(G_OBJECT(device), device_destroyed, self);
+
+    g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
+    g_object_unref(device);
+
+    g_free(driver);
+}
+
+static void
+adsl_remove(NMAtmManager *self, struct udev_device *udev_device)
+{
+    NMAtmManagerPrivate *priv  = NM_ATM_MANAGER_GET_PRIVATE(self);
+    const char *         iface = udev_device_get_sysname(udev_device);
+    GSList *             iter;
+
+    nm_log_dbg(LOGD_PLATFORM, "(%s): removing ATM device", iface);
+
+    for (iter = priv->devices; iter; iter = iter->next) {
+        NMDevice *device = iter->data;
+
+        /* Match 'iface' not 'ip_iface' to the ATM device instead of the
+         * NAS bridge interface or PPPoE interface.
+         */
+        if (g_strcmp0(nm_device_get_iface(device), iface) != 0)
+            continue;
+
+        g_object_weak_unref(G_OBJECT(iter->data), device_destroyed, self);
+        priv->devices = g_slist_remove(priv->devices, device);
+        g_signal_emit_by_name(device, NM_DEVICE_REMOVED);
+        break;
+    }
+}
+
+static void
+start(NMDeviceFactory *factory)
+{
+    NMAtmManager *          self = NM_ATM_MANAGER(factory);
+    NMAtmManagerPrivate *   priv = NM_ATM_MANAGER_GET_PRIVATE(self);
+    struct udev_enumerate * enumerate;
+    struct udev_list_entry *devices;
+
+    enumerate = nm_udev_client_enumerate_new(priv->udev_client);
+    udev_enumerate_add_match_is_initialized(enumerate);
+    udev_enumerate_scan_devices(enumerate);
+    devices = udev_enumerate_get_list_entry(enumerate);
+    for (; devices; devices = udev_list_entry_get_next(devices)) {
+        struct udev_device *udevice;
+
+        udevice = udev_device_new_from_syspath(udev_enumerate_get_udev(enumerate),
+                                               udev_list_entry_get_name(devices));
+        if (udevice) {
+            adsl_add(self, udevice);
+            udev_device_unref(udevice);
+        }
+    }
+    udev_enumerate_unref(enumerate);
+}
+
+static void
+handle_uevent(NMUdevClient *client, struct udev_device *device, gpointer user_data)
+{
+    NMAtmManager *self = NM_ATM_MANAGER(user_data);
+    const char *  subsys;
+    const char *  ifindex;
+    guint64       seqnum;
+    const char *  action;
+
+    action = udev_device_get_action(device);
+
+    g_return_if_fail(action != NULL);
+
+    /* A bit paranoid */
+    subsys = udev_device_get_subsystem(device);
+    g_return_if_fail(!g_strcmp0(subsys, "atm"));
+
+    ifindex = udev_device_get_property_value(device, "IFINDEX");
+    seqnum  = udev_device_get_seqnum(device);
+    nm_log_dbg(LOGD_PLATFORM,
+               "UDEV event: action '%s' subsys '%s' device '%s' (%s); seqnum=%" G_GUINT64_FORMAT,
+               action,
+               subsys,
+               udev_device_get_sysname(device),
+               ifindex ?: "unknown",
+               seqnum);
+
+    if (!strcmp(action, "add"))
+        adsl_add(self, device);
+    else if (!strcmp(action, "remove"))
+        adsl_remove(self, device);
+}
+
+/*****************************************************************************/
+
+static void
+nm_atm_manager_init(NMAtmManager *self)
+{
+    NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self);
+
+    priv->udev_client = nm_udev_client_new(NM_MAKE_STRV("atm"), handle_uevent, self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMAtmManager *       self = NM_ATM_MANAGER(object);
+    NMAtmManagerPrivate *priv = NM_ATM_MANAGER_GET_PRIVATE(self);
+    GSList *             iter;
+
+    for (iter = priv->devices; iter; iter = iter->next)
+        g_object_weak_unref(G_OBJECT(iter->data), device_destroyed, self);
+    nm_clear_pointer(&priv->devices, g_slist_free);
+
+    priv->udev_client = nm_udev_client_destroy(priv->udev_client);
+
+    G_OBJECT_CLASS(nm_atm_manager_parent_class)->dispose(object);
+}
+
+static void
+nm_atm_manager_class_init(NMAtmManagerClass *klass)
+{
+    GObjectClass *        object_class  = G_OBJECT_CLASS(klass);
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->start               = start;
+}
diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c
new file mode 100644
index 00000000..34c062a8
--- /dev/null
+++ b/src/core/devices/adsl/nm-device-adsl.c
@@ -0,0 +1,720 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Pantelis Koukousoulas <pktoss@gmail.com>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-adsl.h"
+
+#include <sys/socket.h>
+#include <linux/atmdev.h>
+#include <linux/atmbr2684.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#include "nm-ip4-config.h"
+#include "devices/nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "ppp/nm-ppp-manager-call.h"
+#include "ppp/nm-ppp-status.h"
+#include "nm-setting-adsl.h"
+#include "nm-utils.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceAdsl
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_ATM_INDEX, );
+
+typedef struct {
+    guint carrier_poll_id;
+    int   atm_index;
+
+    /* PPP */
+    NMPPPManager *ppp_manager;
+
+    /* RFC 2684 bridging (PPPoE over ATM) */
+    int   brfd;
+    int   nas_ifindex;
+    char *nas_ifname;
+    guint nas_update_id;
+    guint nas_update_count;
+} NMDeviceAdslPrivate;
+
+struct _NMDeviceAdsl {
+    NMDevice            parent;
+    NMDeviceAdslPrivate _priv;
+};
+
+struct _NMDeviceAdslClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceAdsl, nm_device_adsl, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_ADSL_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceAdsl, NM_IS_DEVICE_ADSL, NMDevice)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return (NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER
+            | NM_DEVICE_CAP_IS_NON_KERNEL);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingAdsl *s_adsl;
+    const char *   protocol;
+
+    if (!NM_DEVICE_CLASS(nm_device_adsl_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_adsl = nm_connection_get_setting_adsl(connection);
+
+    protocol = nm_setting_adsl_get_protocol(s_adsl);
+    if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_IPOATM)) {
+        /* FIXME: we don't yet support IPoATM */
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "IPoATM protocol is not yet supported");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingAdsl *s_adsl;
+
+    /*
+     * We can't telepathically figure out the username, so if
+     * it wasn't given, we can't complete the connection.
+     */
+    s_adsl = nm_connection_get_setting_adsl(connection);
+    if (s_adsl && !nm_setting_verify(NM_SETTING(s_adsl), NULL, error))
+        return FALSE;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_ADSL_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("ADSL connection"),
+                              NULL,
+                              NULL,
+                              FALSE); /* No IPv6 yet by default */
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+br2684_assign_vcc(NMDeviceAdsl *self, NMSettingAdsl *s_adsl)
+{
+    NMDeviceAdslPrivate *     priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    struct sockaddr_atmpvc    addr;
+    struct atm_backend_br2684 be;
+    struct atm_qos            qos;
+    int                       errsv, err, bufsize = 8192;
+    const char *              encapsulation;
+    gboolean                  is_llc;
+
+    g_return_val_if_fail(priv->brfd == -1, FALSE);
+    g_return_val_if_fail(priv->nas_ifname != NULL, FALSE);
+
+    priv->brfd = socket(PF_ATMPVC, SOCK_DGRAM | SOCK_CLOEXEC, ATM_AAL5);
+    if (priv->brfd < 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to open ATM control socket (%d)", errsv);
+        priv->brfd = -1;
+        return FALSE;
+    }
+
+    err = setsockopt(priv->brfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
+    if (err != 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to set SNDBUF option (%d)", errsv);
+        goto error;
+    }
+
+    /* QoS */
+    memset(&qos, 0, sizeof(qos));
+    qos.aal                = ATM_AAL5;
+    qos.txtp.traffic_class = ATM_UBR;
+    qos.txtp.max_sdu       = 1524;
+    qos.txtp.pcr           = ATM_MAX_PCR;
+    qos.rxtp               = qos.txtp;
+
+    err = setsockopt(priv->brfd, SOL_ATM, SO_ATMQOS, &qos, sizeof(qos));
+    if (err != 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to set QoS (%d)", errsv);
+        goto error;
+    }
+
+    encapsulation = nm_setting_adsl_get_encapsulation(s_adsl);
+
+    /* VPI/VCI */
+    memset(&addr, 0, sizeof(addr));
+    addr.sap_family   = AF_ATMPVC;
+    addr.sap_addr.itf = priv->atm_index;
+    addr.sap_addr.vpi = (guint16) nm_setting_adsl_get_vpi(s_adsl);
+    addr.sap_addr.vci = (int) nm_setting_adsl_get_vci(s_adsl);
+
+    _LOGD(LOGD_ADSL,
+          "assigning address %d.%d.%d encapsulation %s",
+          priv->atm_index,
+          addr.sap_addr.vpi,
+          addr.sap_addr.vci,
+          encapsulation ?: "(none)");
+
+    err = connect(priv->brfd, (struct sockaddr *) &addr, sizeof(addr));
+    if (err != 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to set VPI/VCI (%d)", errsv);
+        goto error;
+    }
+
+    /* And last attach the VCC to the interface */
+    is_llc = (g_strcmp0(encapsulation, "llc") == 0);
+
+    memset(&be, 0, sizeof(be));
+    be.backend_num   = ATM_BACKEND_BR2684;
+    be.ifspec.method = BR2684_FIND_BYIFNAME;
+    nm_utils_ifname_cpy(be.ifspec.spec.ifname, priv->nas_ifname);
+    be.fcs_in  = BR2684_FCSIN_NO;
+    be.fcs_out = BR2684_FCSOUT_NO;
+    be.encaps  = is_llc ? BR2684_ENCAPS_LLC : BR2684_ENCAPS_VC;
+    err        = ioctl(priv->brfd, ATM_SETBACKEND, &be);
+    if (err != 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to attach VCC (%d)", errsv);
+        goto error;
+    }
+
+    return TRUE;
+
+error:
+    nm_close(priv->brfd);
+    priv->brfd = -1;
+    return FALSE;
+}
+
+static void
+link_changed_cb(NMPlatform *    platform,
+                int             obj_type_i,
+                int             ifindex,
+                NMPlatformLink *info,
+                int             change_type_i,
+                NMDeviceAdsl *  self)
+{
+    const NMPlatformSignalChangeType change_type = change_type_i;
+
+    if (change_type == NM_PLATFORM_SIGNAL_REMOVED) {
+        NMDeviceAdslPrivate *priv   = NM_DEVICE_ADSL_GET_PRIVATE(self);
+        NMDevice *           device = NM_DEVICE(self);
+
+        /* This only gets called for PPPoE connections and "nas" interfaces */
+
+        if (priv->nas_ifindex > 0 && ifindex == priv->nas_ifindex) {
+            /* NAS device went away for some reason; kill the connection */
+            _LOGD(LOGD_ADSL, "br2684 interface disappeared");
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_BR2684_FAILED);
+        }
+    }
+}
+
+static gboolean
+pppoe_vcc_config(NMDeviceAdsl *self)
+{
+    NMDeviceAdslPrivate *priv   = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+    NMSettingAdsl *      s_adsl;
+
+    s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL);
+
+    g_return_val_if_fail(s_adsl, FALSE);
+
+    /* Set up the VCC */
+    if (!br2684_assign_vcc(self, s_adsl))
+        return FALSE;
+
+    /* Watch for the 'nas' interface going away */
+    g_signal_connect(nm_device_get_platform(device),
+                     NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                     G_CALLBACK(link_changed_cb),
+                     self);
+
+    _LOGD(LOGD_ADSL, "ATM setup successful");
+
+    /* otherwise we're good for stage3 */
+    nm_platform_link_set_up(nm_device_get_platform(device), priv->nas_ifindex, NULL);
+
+    return TRUE;
+}
+
+static gboolean
+nas_update_cb(gpointer user_data)
+{
+    NMDeviceAdsl *       self   = NM_DEVICE_ADSL(user_data);
+    NMDeviceAdslPrivate *priv   = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+
+    nm_assert(priv->nas_ifname);
+
+    priv->nas_update_count++;
+
+    nm_assert(priv->nas_ifindex <= 0);
+    priv->nas_ifindex =
+        nm_platform_link_get_ifindex(nm_device_get_platform(device), priv->nas_ifname);
+    if (priv->nas_ifindex <= 0) {
+        if (priv->nas_update_count <= 10) {
+            /* Keep waiting for it to appear */
+            return G_SOURCE_CONTINUE;
+        }
+        priv->nas_update_id = 0;
+        _LOGW(LOGD_ADSL,
+              "failed to find br2684 interface %s ifindex after timeout",
+              priv->nas_ifname);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BR2684_FAILED);
+        return G_SOURCE_REMOVE;
+    }
+
+    priv->nas_update_id = 0;
+    _LOGD(LOGD_ADSL, "using br2684 iface '%s' index %d", priv->nas_ifname, priv->nas_ifindex);
+
+    if (!pppoe_vcc_config(self)) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BR2684_FAILED);
+        return G_SOURCE_REMOVE;
+    }
+
+    nm_device_activate_schedule_stage2_device_config(device, TRUE);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+br2684_create_iface(NMDeviceAdsl *self)
+{
+    NMDeviceAdslPrivate *   priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    struct atm_newif_br2684 ni;
+    nm_auto_close int       fd = -1;
+    int                     err, errsv;
+    guint                   num = 0;
+
+    if (nm_clear_g_source(&priv->nas_update_id))
+        nm_assert_not_reached();
+
+    fd = socket(PF_ATMPVC, SOCK_DGRAM | SOCK_CLOEXEC, ATM_AAL5);
+    if (fd < 0) {
+        errsv = errno;
+        _LOGE(LOGD_ADSL, "failed to open ATM control socket (%d)", errsv);
+        return FALSE;
+    }
+
+    memset(&ni, 0, sizeof(ni));
+    ni.backend_num = ATM_BACKEND_BR2684;
+    ni.media       = BR2684_MEDIA_ETHERNET;
+    ni.mtu         = 1500;
+
+    /* Loop attempting to create an interface that doesn't exist yet.  The
+     * kernel can create one for us automatically, but due to API issues it
+     * cannot return that name to us.  Since we want to know the name right
+     * away, just brute-force it.
+     */
+    while (TRUE) {
+        memset(&ni.ifname, 0, sizeof(ni.ifname));
+        g_snprintf(ni.ifname, sizeof(ni.ifname), "nas%u", num++);
+
+        err = ioctl(fd, ATM_NEWBACKENDIF, &ni);
+        if (err != 0) {
+            errsv = errno;
+            if (errsv == EEXIST)
+                continue;
+
+            _LOGW(LOGD_ADSL, "failed to create br2684 interface (%d)", errsv);
+            return FALSE;
+        }
+
+        nm_utils_strdup_reset(&priv->nas_ifname, ni.ifname);
+        _LOGD(LOGD_ADSL, "waiting for br2684 iface '%s' to appear", priv->nas_ifname);
+        priv->nas_update_count = 0;
+        priv->nas_update_id    = g_timeout_add(100, nas_update_cb, self);
+        return TRUE;
+    }
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceAdsl *       self = NM_DEVICE_ADSL(device);
+    NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    NMSettingAdsl *      s_adsl;
+    const char *         protocol;
+
+    s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL);
+
+    g_return_val_if_fail(s_adsl, NM_ACT_STAGE_RETURN_FAILURE);
+
+    protocol = nm_setting_adsl_get_protocol(s_adsl);
+    _LOGD(LOGD_ADSL, "using ADSL protocol '%s'", protocol);
+
+    if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_PPPOA)) {
+        /* PPPoA doesn't need anything special */
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    if (nm_streq0(protocol, NM_SETTING_ADSL_PROTOCOL_PPPOE)) {
+        /* PPPoE needs RFC2684 bridging before we can do PPP over it */
+        if (priv->nas_ifindex <= 0) {
+            if (priv->nas_update_id == 0) {
+                if (!br2684_create_iface(self)) {
+                    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_BR2684_FAILED);
+                    return NM_ACT_STAGE_RETURN_FAILURE;
+                }
+            }
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    _LOGW(LOGD_ADSL, "unhandled ADSL protocol '%s'", protocol);
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    switch (status) {
+    case NM_PPP_STATUS_DISCONNECT:
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
+        break;
+    case NM_PPP_STATUS_DEAD:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_set_ip_ifindex(device, ifindex)) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    }
+}
+
+static void
+ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Ignore PPP IP4 events that come in after initial configuration */
+    if (nm_device_activate_ip4_state_in_conf(device))
+        nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config));
+}
+
+static NMActStageReturn
+act_stage3_ip4_config_start(NMDevice *           device,
+                            NMIP4Config **       out_config,
+                            NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceAdsl *       self = NM_DEVICE_ADSL(device);
+    NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+    NMSettingAdsl *      s_adsl;
+    NMActRequest *       req;
+    GError *             err = NULL;
+    const char *         ppp_iface;
+
+    req = nm_device_get_act_request(device);
+
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_adsl = nm_device_get_applied_setting(device, NM_TYPE_SETTING_ADSL);
+
+    g_return_val_if_fail(s_adsl, NM_ACT_STAGE_RETURN_FAILURE);
+
+    /* PPPoE uses the NAS interface, not the ATM interface */
+    if (nm_streq0(nm_setting_adsl_get_protocol(s_adsl), NM_SETTING_ADSL_PROTOCOL_PPPOE)) {
+        nm_assert(priv->nas_ifname);
+        ppp_iface = priv->nas_ifname;
+
+        _LOGD(LOGD_ADSL, "starting PPPoE on br2684 interface %s", priv->nas_ifname);
+    } else {
+        ppp_iface = nm_device_get_iface(device);
+        _LOGD(LOGD_ADSL, "starting PPPoA");
+    }
+
+    priv->ppp_manager = nm_ppp_manager_create(ppp_iface, &err);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            nm_device_get_route_table(device, AF_INET),
+                                            nm_device_get_route_metric(device, AF_INET),
+                                            nm_device_get_route_table(device, AF_INET6),
+                                            nm_device_get_route_metric(device, AF_INET6));
+    }
+
+    if (!priv->ppp_manager
+        || !nm_ppp_manager_start(priv->ppp_manager,
+                                 req,
+                                 nm_setting_adsl_get_username(s_adsl),
+                                 30,
+                                 0,
+                                 &err)) {
+        _LOGW(LOGD_ADSL, "PPP failed to start: %s", err->message);
+        g_error_free(err);
+
+        g_clear_object(&priv->ppp_manager);
+
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
+                     G_CALLBACK(ppp_state_changed),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
+                     G_CALLBACK(ppp_ifindex_set),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
+                     G_CALLBACK(ppp_ip4_config),
+                     self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    if (addr_family == AF_INET)
+        return act_stage3_ip4_config_start(device, (NMIP4Config **) out_config, out_failure_reason);
+
+    return NM_DEVICE_CLASS(nm_device_adsl_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static void
+adsl_cleanup(NMDeviceAdsl *self)
+{
+    NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+
+    if (priv->ppp_manager) {
+        g_signal_handlers_disconnect_by_func(priv->ppp_manager,
+                                             G_CALLBACK(ppp_state_changed),
+                                             self);
+        g_signal_handlers_disconnect_by_func(priv->ppp_manager, G_CALLBACK(ppp_ip4_config), self);
+        nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
+        g_clear_object(&priv->ppp_manager);
+    }
+
+    g_signal_handlers_disconnect_by_func(nm_device_get_platform(NM_DEVICE(self)),
+                                         G_CALLBACK(link_changed_cb),
+                                         self);
+
+    nm_close(priv->brfd);
+    priv->brfd = -1;
+
+    nm_clear_g_source(&priv->nas_update_id);
+
+    /* FIXME: kernel has no way of explicitly deleting the 'nasX' interface yet,
+     * so it gets leaked.  It does get destroyed when it's no longer in use,
+     * but we have no control over that.
+     */
+    priv->nas_ifindex = 0;
+    nm_clear_g_free(&priv->nas_ifname);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    adsl_cleanup(NM_DEVICE_ADSL(device));
+}
+
+/*****************************************************************************/
+
+static gboolean
+carrier_update_cb(gpointer user_data)
+{
+    NMDeviceAdsl *self = NM_DEVICE_ADSL(user_data);
+    int           carrier;
+    char *        path;
+
+    path    = g_strdup_printf("/sys/class/atm/%s/carrier",
+                           NM_ASSERT_VALID_PATH_COMPONENT(nm_device_get_iface(NM_DEVICE(self))));
+    carrier = (int) nm_platform_sysctl_get_int_checked(nm_device_get_platform(NM_DEVICE(self)),
+                                                       NMP_SYSCTL_PATHID_ABSOLUTE(path),
+                                                       10,
+                                                       0,
+                                                       1,
+                                                       -1);
+    g_free(path);
+
+    if (carrier != -1)
+        nm_device_set_carrier(NM_DEVICE(self), carrier);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    switch (prop_id) {
+    case PROP_ATM_INDEX:
+        g_value_set_int(value, NM_DEVICE_ADSL_GET_PRIVATE(object)->atm_index);
+        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)
+{
+    switch (prop_id) {
+    case PROP_ATM_INDEX:
+        /* construct-only */
+        NM_DEVICE_ADSL_GET_PRIVATE(object)->atm_index = g_value_get_int(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_adsl_init(NMDeviceAdsl *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceAdsl *       self = NM_DEVICE_ADSL(object);
+    NMDeviceAdslPrivate *priv = NM_DEVICE_ADSL_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_device_adsl_parent_class)->constructed(object);
+
+    priv->carrier_poll_id = g_timeout_add_seconds(5, carrier_update_cb, self);
+
+    _LOGD(LOGD_ADSL, "ATM device index %d", priv->atm_index);
+
+    g_return_if_fail(priv->atm_index >= 0);
+}
+
+NMDevice *
+nm_device_adsl_new(const char *udi, const char *iface, const char *driver, int atm_index)
+{
+    g_return_val_if_fail(udi != NULL, NULL);
+    g_return_val_if_fail(atm_index >= 0, NULL);
+
+    return g_object_new(NM_TYPE_DEVICE_ADSL,
+                        NM_DEVICE_UDI,
+                        udi,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_DRIVER,
+                        driver,
+                        NM_DEVICE_ADSL_ATM_INDEX,
+                        atm_index,
+                        NM_DEVICE_TYPE_DESC,
+                        "ADSL",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_ADSL,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    adsl_cleanup(NM_DEVICE_ADSL(object));
+
+    nm_clear_g_source(&NM_DEVICE_ADSL_GET_PRIVATE(object)->carrier_poll_id);
+
+    G_OBJECT_CLASS(nm_device_adsl_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_adsl = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_ADSL,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier",
+                                                             "b",
+                                                             NM_DEVICE_CARRIER), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_adsl_class_init(NMDeviceAdslClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_adsl);
+
+    device_class->connection_type_check_compatible = NM_SETTING_ADSL_SETTING_NAME;
+
+    device_class->get_generic_capabilities = get_generic_capabilities;
+
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+
+    device_class->act_stage2_config          = act_stage2_config;
+    device_class->act_stage3_ip_config_start = act_stage3_ip_config_start;
+    device_class->deactivate                 = deactivate;
+
+    obj_properties[PROP_ATM_INDEX] =
+        g_param_spec_int(NM_DEVICE_ADSL_ATM_INDEX,
+                         "",
+                         "",
+                         -1,
+                         G_MAXINT,
+                         -1,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/adsl/nm-device-adsl.h b/src/core/devices/adsl/nm-device-adsl.h
new file mode 100644
index 00000000..c5c18901
--- /dev/null
+++ b/src/core/devices/adsl/nm-device-adsl.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Author: Pantelis Koukousoulas <pktoss@gmail.com>
+ * Copyright (C) 2009 - 2011 Red Hat Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_ADSL_H__
+#define __NETWORKMANAGER_DEVICE_ADSL_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_ADSL (nm_device_adsl_get_type())
+#define NM_DEVICE_ADSL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdsl))
+#define NM_DEVICE_ADSL_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ADSL, NMDeviceAdslClass))
+#define NM_IS_DEVICE_ADSL(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ADSL))
+#define NM_IS_DEVICE_ADSL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_ADSL))
+#define NM_DEVICE_ADSL_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_ADSL, NMDeviceAdslClass))
+
+#define NM_DEVICE_ADSL_ATM_INDEX "atm-index"
+
+typedef struct _NMDeviceAdsl      NMDeviceAdsl;
+typedef struct _NMDeviceAdslClass NMDeviceAdslClass;
+
+GType nm_device_adsl_get_type(void);
+
+NMDevice *nm_device_adsl_new(const char *udi, const char *iface, const char *driver, int atm_index);
+
+#endif /* NM_DEVICE_ADSL_H */
diff --git a/src/core/devices/bluetooth/meson.build b/src/core/devices/bluetooth/meson.build
new file mode 100644
index 00000000..d5f26068
--- /dev/null
+++ b/src/core/devices/bluetooth/meson.build
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_device_plugin_bluetooth_static = static_library(
+  'nm-device-plugin-bluetooth-static',
+  sources: files(
+    'nm-bluez-manager.c',
+    'nm-bt-error.c',
+    'nm-device-bt.c',
+  ) + (enable_bluez5_dun ? files('nm-bluez5-dun.c') : files()),
+  dependencies: [
+    core_default_dep,
+    libnm_wwan_dep,
+    bluez5_dep,
+  ],
+  c_args: daemon_c_flags,
+)
+
+libnm_device_plugin_bluetooth_static_dep = declare_dependency(
+  link_whole: libnm_device_plugin_bluetooth_static,
+)
+
+libnm_device_plugin_bluetooth = shared_module(
+  'nm-device-plugin-bluetooth',
+  dependencies: [
+    core_plugin_dep,
+    libnm_wwan_dep,
+    bluez5_dep,
+    libnm_device_plugin_bluetooth_static_dep,
+  ],
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+  install_rpath: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_bluetooth
+
+test(
+  'check-local-devices-bluetooth',
+  check_exports,
+  args: [
+    libnm_device_plugin_bluetooth.full_path(),
+    linker_script_devices
+  ],
+)
+
+if enable_tests
+  executable(
+    'nm-bt-test',
+    'tests/nm-bt-test.c',
+    dependencies: [
+      libNetworkManagerTest_dep,
+      core_default_dep,
+      libnm_wwan_dep,
+      bluez5_dep,
+      libnm_device_plugin_bluetooth_static_dep,
+    ],
+    c_args: test_c_flags,
+  )
+endif
diff --git a/src/core/devices/bluetooth/nm-bluez-common.h b/src/core/devices/bluetooth/nm-bluez-common.h
new file mode 100644
index 00000000..868a985e
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-common.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_BLUEZ_COMMON_H__
+#define __NETWORKMANAGER_BLUEZ_COMMON_H__
+
+#define BLUETOOTH_CONNECT_DUN "dun"
+#define BLUETOOTH_CONNECT_NAP "nap"
+
+#define NM_BLUEZ_SERVICE "org.bluez"
+
+#define NM_BLUEZ_MANAGER_PATH "/"
+
+#define NM_BLUEZ5_ADAPTER_INTERFACE        "org.bluez.Adapter1"
+#define NM_BLUEZ5_DEVICE_INTERFACE         "org.bluez.Device1"
+#define NM_BLUEZ5_NETWORK_INTERFACE        "org.bluez.Network1"
+#define NM_BLUEZ5_NETWORK_SERVER_INTERFACE "org.bluez.NetworkServer1"
+
+#define NM_BLUEZ_MANAGER_BDADDR_ADDED         "bdaddr-added"
+#define NM_BLUEZ_MANAGER_NETWORK_SERVER_ADDED "network-server-added"
+
+#endif /* NM_BLUEZ_COMMON_H */
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.c b/src/core/devices/bluetooth/nm-bluez-manager.c
new file mode 100644
index 00000000..dd998d29
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-manager.c
@@ -0,0 +1,2896 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-bluez-manager.h"
+
+#include <signal.h>
+#include <stdlib.h>
+#include <gmodule.h>
+#include <linux/if_ether.h>
+
+#include "nm-glib-aux/nm-dbus-aux.h"
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-dbus-manager.h"
+#include "devices/nm-device-factory.h"
+#include "devices/nm-device-bridge.h"
+#include "nm-setting-bluetooth.h"
+#include "settings/nm-settings.h"
+#include "nm-bluez-common.h"
+#include "nm-device-bt.h"
+#include "nm-manager.h"
+#include "nm-bluez5-dun.h"
+#include "nm-core-internal.h"
+#include "platform/nm-platform.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+
+/*****************************************************************************/
+
+#if WITH_BLUEZ5_DUN
+    #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_DUN
+#else
+    #define _NM_BT_CAPABILITY_SUPPORTED_DUN NM_BT_CAPABILITY_NONE
+#endif
+#define _NM_BT_CAPABILITY_SUPPORTED (NM_BT_CAPABILITY_NAP | _NM_BT_CAPABILITY_SUPPORTED_DUN)
+
+typedef struct {
+    const char *            bdaddr;
+    CList                   lst_head;
+    NMBluetoothCapabilities bt_type : 8;
+    char                    bdaddr_data[];
+} ConnDataHead;
+
+typedef struct {
+    NMSettingsConnection *sett_conn;
+    ConnDataHead *        cdata_hd;
+    CList                 lst;
+} ConnDataElem;
+
+typedef struct {
+    GCancellable *             ext_cancellable;
+    GCancellable *             int_cancellable;
+    NMBtVTableRegisterCallback callback;
+    gpointer                   callback_user_data;
+    gulong                     ext_cancelled_id;
+} NetworkServerRegisterReqData;
+
+typedef struct {
+    GCancellable *          ext_cancellable;
+    GCancellable *          int_cancellable;
+    NMBluezManagerConnectCb callback;
+    gpointer                callback_user_data;
+    char *                  device_name;
+    gulong                  ext_cancelled_id;
+    guint                   timeout_id;
+    guint                   timeout_wait_connect_id;
+} DeviceConnectReqData;
+
+typedef struct {
+    const char *object_path;
+
+    NMBluezManager *self;
+
+    /* Fields name with "d_" prefix are purely cached values from BlueZ's
+     * ObjectManager D-Bus interface. There is no logic whatsoever about
+     * them.
+     */
+
+    CList process_change_lst;
+
+    struct {
+        char *address;
+    } d_adapter;
+
+    struct {
+        char *address;
+        char *name;
+        char *adapter;
+    } d_device;
+
+    struct {
+        char *interface;
+    } d_network;
+
+    struct {
+        CList                         lst;
+        char *                        adapter_address;
+        NMDevice *                    device_br;
+        NetworkServerRegisterReqData *r_req_data;
+    } x_network_server;
+
+    struct {
+        NMSettingsConnection *panu_connection;
+        NMDeviceBt *          device_bt;
+        DeviceConnectReqData *c_req_data;
+        NMBluez5DunContext *  connect_dun_context;
+        gulong                device_bt_signal_id;
+    } x_device;
+
+    /* indicate whether the D-Bus object has the particular D-Bus interface. */
+    bool d_has_adapter_iface : 1;
+    bool d_has_device_iface : 1;
+    bool d_has_network_iface : 1;
+    bool d_has_network_server_iface : 1;
+
+    /* cached D-Bus properties for Device1 ("d_device*"). */
+    NMBluetoothCapabilities d_device_capabilities : 6;
+    bool                    d_device_connected : 1;
+    bool                    d_device_paired : 1;
+
+    /* cached D-Bus properties for Network1 ("d_network*"). */
+    bool d_network_connected : 1;
+
+    /* cached D-Bus properties for Adapter1 ("d_adapter*"). */
+    bool d_adapter_powered : 1;
+
+    /* properties related to device ("x_device*"). */
+    NMBluetoothCapabilities x_device_connect_bt_type : 6;
+    bool                    x_device_is_usable : 1;
+    bool                    x_device_is_connected : 1;
+
+    bool x_device_panu_connection_allow_create : 1;
+
+    /* flag to remember last time when we checked wether the object
+     * was  a suitable adapter that is usable to a device. */
+    bool was_usable_adapter_for_device_before : 1;
+
+    char _object_path_intern[];
+} BzDBusObj;
+
+typedef struct {
+    NMManager * manager;
+    NMSettings *settings;
+
+    GDBusConnection *dbus_connection;
+
+    NMBtVTableNetworkServer vtable_network_server;
+
+    GCancellable *name_owner_get_cancellable;
+    GCancellable *get_managed_objects_cancellable;
+
+    GHashTable *bzobjs;
+
+    char *name_owner;
+
+    GHashTable *conn_data_heads;
+    GHashTable *conn_data_elems;
+
+    CList network_server_lst_head;
+
+    CList process_change_lst_head;
+
+    guint name_owner_changed_id;
+
+    guint managed_objects_changed_id;
+
+    guint properties_changed_id;
+
+    guint process_change_idle_id;
+
+    bool settings_registered : 1;
+} NMBluezManagerPrivate;
+
+struct _NMBluezManager {
+    NMDeviceFactory       parent;
+    NMBluezManagerPrivate _priv;
+};
+
+struct _NMBluezManagerClass {
+    NMDeviceFactoryClass parent;
+};
+
+G_DEFINE_TYPE(NMBluezManager, nm_bluez_manager, NM_TYPE_DEVICE_FACTORY);
+
+#define NM_BLUEZ_MANAGER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMBluezManager, NM_IS_BLUEZ_MANAGER)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(
+    NM_LINK_TYPE_BNEP) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BLUETOOTH_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_BLUEZ_MANAGER, NULL);
+}
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_BT
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "bluez", __VA_ARGS__)
+
+/*****************************************************************************/
+
+static NMBluetoothCapabilities
+convert_uuids_to_capabilities(const char *const *strv)
+{
+    NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+    if (strv) {
+        for (; strv[0]; strv++) {
+            gs_free char *s_part1 = NULL;
+            const char *  str     = strv[0];
+            const char *  s;
+
+            s = strchr(str, '-');
+            if (!s)
+                continue;
+
+            s_part1 = g_strndup(str, s - str);
+            switch (_nm_utils_ascii_str_to_int64(s_part1, 16, 0, G_MAXINT, -1)) {
+            case 0x1103:
+                capabilities |= NM_BT_CAPABILITY_DUN;
+                break;
+            case 0x1116:
+                capabilities |= NM_BT_CAPABILITY_NAP;
+                break;
+            default:
+                break;
+            }
+        }
+    }
+
+    return capabilities;
+}
+
+/*****************************************************************************/
+
+static void     _cleanup_for_name_owner(NMBluezManager *self);
+static void     _connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason);
+static gboolean _bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered);
+static gboolean _bzobjs_is_dead(const BzDBusObj *bzobj);
+static gboolean _bzobjs_device_is_usable(const BzDBusObj *bzobj,
+                                         BzDBusObj **     out_adapter_bzobj,
+                                         gboolean *       out_create_panu_connection);
+static gboolean _bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj);
+static ConnDataHead *
+_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr);
+static void _process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj);
+static void
+_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason);
+static gboolean _connect_timeout_wait_connected_cb(gpointer user_data);
+
+/*****************************************************************************/
+
+static void
+_dbus_call_complete_cb_nop(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    /* we don't do anything at all. The only reason to register this
+     * callback is so that GDBusConnection keeps the cancellable alive
+     * long enough until the call completes.
+     *
+     * Note that this cancellable in turn is registered via
+     * nm_shutdown_wait_obj_register_*(), to block shutdown until
+     * we are done. */
+}
+
+/*****************************************************************************/
+
+static void
+_network_server_register_req_data_complete(NetworkServerRegisterReqData *r_req_data, GError *error)
+{
+    nm_clear_g_signal_handler(r_req_data->ext_cancellable, &r_req_data->ext_cancelled_id);
+
+    nm_clear_g_cancellable(&r_req_data->int_cancellable);
+
+    if (r_req_data->callback) {
+        gs_free_error GError *error_cancelled = NULL;
+
+        if (g_cancellable_set_error_if_cancelled(r_req_data->ext_cancellable, &error_cancelled))
+            error = error_cancelled;
+
+        r_req_data->callback(error, r_req_data->callback_user_data);
+    }
+
+    g_object_unref(r_req_data->ext_cancellable);
+    nm_g_slice_free(r_req_data);
+}
+
+static void
+_device_connect_req_data_complete(DeviceConnectReqData *c_req_data,
+                                  NMBluezManager *      self,
+                                  const char *          device_name,
+                                  GError *              error)
+{
+    nm_assert((!!device_name) != (!!error));
+
+    nm_clear_g_signal_handler(c_req_data->ext_cancellable, &c_req_data->ext_cancelled_id);
+
+    nm_clear_g_cancellable(&c_req_data->int_cancellable);
+    nm_clear_g_source(&c_req_data->timeout_id);
+    nm_clear_g_source(&c_req_data->timeout_wait_connect_id);
+
+    if (c_req_data->callback) {
+        gs_free_error GError *error_cancelled = NULL;
+
+        if (g_cancellable_set_error_if_cancelled(c_req_data->ext_cancellable, &error_cancelled)) {
+            error       = error_cancelled;
+            device_name = NULL;
+        }
+
+        c_req_data->callback(self, TRUE, device_name, error, c_req_data->callback_user_data);
+    }
+
+    g_object_unref(c_req_data->ext_cancellable);
+    nm_clear_g_free(&c_req_data->device_name);
+    nm_g_slice_free(c_req_data);
+}
+
+/*****************************************************************************/
+
+static BzDBusObj *
+_bz_dbus_obj_new(NMBluezManager *self, const char *object_path)
+{
+    BzDBusObj *bzobj;
+    gsize      l;
+
+    nm_assert(NM_IS_BLUEZ_MANAGER(self));
+
+    l = strlen(object_path) + 1;
+
+    bzobj  = g_malloc(sizeof(BzDBusObj) + l);
+    *bzobj = (BzDBusObj){
+        .object_path                           = bzobj->_object_path_intern,
+        .self                                  = self,
+        .x_network_server.lst                  = C_LIST_INIT(bzobj->x_network_server.lst),
+        .process_change_lst                    = C_LIST_INIT(bzobj->process_change_lst),
+        .x_device_panu_connection_allow_create = TRUE,
+    };
+    memcpy(bzobj->_object_path_intern, object_path, l);
+
+    return bzobj;
+}
+
+static void
+_bz_dbus_obj_free(BzDBusObj *bzobj)
+{
+    nm_assert(bzobj);
+    nm_assert(NM_IS_BLUEZ_MANAGER(bzobj->self));
+    nm_assert(!bzobj->x_network_server.device_br);
+    nm_assert(!bzobj->x_network_server.r_req_data);
+    nm_assert(!bzobj->x_device.c_req_data);
+
+    c_list_unlink_stale(&bzobj->process_change_lst);
+    c_list_unlink_stale(&bzobj->x_network_server.lst);
+    g_free(bzobj->x_network_server.adapter_address);
+    g_free(bzobj->d_adapter.address);
+    g_free(bzobj->d_network.interface);
+    g_free(bzobj->d_device.address);
+    g_free(bzobj->d_device.name);
+    g_free(bzobj->d_device.adapter);
+    g_free(bzobj);
+}
+
+/*****************************************************************************/
+
+static const char *
+_bzobj_to_string(const BzDBusObj *bzobj, char *buf, gsize len)
+{
+    char *      buf0   = buf;
+    const char *prefix = "";
+    gboolean    device_is_usable;
+    gboolean    create_panu_connection = FALSE;
+    gboolean    network_server_is_usable;
+    char        sbuf_cap[100];
+
+    if (len > 0)
+        buf[0] = '\0';
+
+    if (bzobj->d_has_adapter_iface) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Adapter1 {");
+        if (bzobj->d_adapter.address) {
+            nm_utils_strbuf_append(&buf, &len, " d.address: \"%s\"", bzobj->d_adapter.address);
+            if (bzobj->d_adapter_powered)
+                nm_utils_strbuf_append_str(&buf, &len, ",");
+        }
+        if (bzobj->d_adapter_powered)
+            nm_utils_strbuf_append(&buf, &len, " d.powered: 1");
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    if (bzobj->d_has_device_iface) {
+        const char *prefix1 = "";
+
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Device1 {");
+        if (bzobj->d_device.address) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.address: \"%s\"",
+                                   prefix1,
+                                   bzobj->d_device.address);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device.name) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.name: \"%s\"", prefix1, bzobj->d_device.name);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device.adapter) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.adapter: \"%s\"",
+                                   prefix1,
+                                   bzobj->d_device.adapter);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "%s d.capabilities: \"%s\"",
+                                   prefix1,
+                                   nm_bluetooth_capability_to_string(bzobj->d_device_capabilities,
+                                                                     sbuf_cap,
+                                                                     sizeof(sbuf_cap)));
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_connected) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.connected: 1", prefix1);
+            prefix1 = ",";
+        }
+        if (bzobj->d_device_paired) {
+            nm_utils_strbuf_append(&buf, &len, "%s d.paired: 1", prefix1);
+            prefix1 = ",";
+        }
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+    if (bzobj->d_has_network_server_iface
+        || network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst))
+        || !c_list_is_empty(&bzobj->x_network_server.lst)
+        || !nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                      bzobj->x_network_server.adapter_address)
+        || bzobj->x_network_server.device_br || bzobj->x_network_server.r_req_data) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+
+        nm_utils_strbuf_append(&buf, &len, "NetworkServer1 { ");
+
+        if (!bzobj->d_has_network_server_iface)
+            nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, ");
+
+        if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst)))
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   "usable: %d, used: %d",
+                                   !!network_server_is_usable,
+                                   !network_server_is_usable);
+        else if (network_server_is_usable)
+            nm_utils_strbuf_append(&buf, &len, "used: 1");
+        else
+            nm_utils_strbuf_append(&buf, &len, "usable: 0");
+
+        if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                       bzobj->x_network_server.adapter_address)) {
+            if (bzobj->x_network_server.adapter_address)
+                nm_utils_strbuf_append(&buf,
+                                       &len,
+                                       ", adapter-address: \"%s\"",
+                                       bzobj->x_network_server.adapter_address);
+            else
+                nm_utils_strbuf_append(&buf, &len, ", adapter-address: <NULL>");
+        }
+
+        if (bzobj->x_network_server.device_br)
+            nm_utils_strbuf_append(&buf, &len, ", bridge-device: 1");
+
+        if (bzobj->x_network_server.r_req_data)
+            nm_utils_strbuf_append(&buf, &len, ", register-in-progress: 1");
+
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection);
+
+    if (bzobj->d_has_network_iface || bzobj->d_network.interface || bzobj->d_network_connected
+        || create_panu_connection || bzobj->x_device.panu_connection
+        || device_is_usable != bzobj->x_device_is_usable || bzobj->x_device.device_bt
+        || bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+        || bzobj->x_device.connect_dun_context || bzobj->x_device.c_req_data
+        || bzobj->x_device_is_connected != bzobj->d_network_connected) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "Network1 {");
+        if (bzobj->d_network.interface)
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   " d.interface: \"%s\", ",
+                                   bzobj->d_network.interface);
+        if (bzobj->d_network_connected)
+            nm_utils_strbuf_append(&buf, &len, " d.connected: %d, ", !!bzobj->d_network_connected);
+        if (!bzobj->d_has_network_iface)
+            nm_utils_strbuf_append(&buf, &len, " has-d-iface: 0, ");
+        if (device_is_usable != bzobj->x_device_is_usable)
+            nm_utils_strbuf_append(&buf,
+                                   &len,
+                                   " usable: %d, used: %d",
+                                   !!device_is_usable,
+                                   !device_is_usable);
+        else if (device_is_usable)
+            nm_utils_strbuf_append(&buf, &len, " used: 1");
+        else
+            nm_utils_strbuf_append(&buf, &len, " usable: 0");
+
+        if (create_panu_connection)
+            nm_utils_strbuf_append(&buf, &len, ", create-panu-connection: 1");
+
+        if (bzobj->x_device.panu_connection)
+            nm_utils_strbuf_append(&buf, &len, ", has-panu-connection: 1");
+
+        if (bzobj->x_device.device_bt)
+            nm_utils_strbuf_append(&buf, &len, ", has-device: 1");
+
+        if (bzobj->x_device_connect_bt_type != NM_BT_CAPABILITY_NONE
+            || bzobj->x_device.connect_dun_context) {
+            nm_utils_strbuf_append(
+                &buf,
+                &len,
+                ", connect: %s%s",
+                nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                                  sbuf_cap,
+                                                  sizeof(sbuf_cap)),
+                bzobj->x_device.connect_dun_context ? ",with-dun-context" : "");
+        }
+
+        if (bzobj->x_device.c_req_data)
+            nm_utils_strbuf_append(&buf, &len, ", connecting: 1");
+
+        if (bzobj->x_device_is_connected != bzobj->d_network_connected)
+            nm_utils_strbuf_append(&buf, &len, ", connected: %d", !!bzobj->x_device_is_connected);
+
+        nm_utils_strbuf_append_str(&buf, &len, " }");
+    }
+
+    if (_bzobjs_is_dead(bzobj)) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append_str(&buf, &len, "dead: 1");
+    }
+
+    if (!c_list_is_empty(&bzobj->process_change_lst)) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append(&buf, &len, "change-pending-on-idle: 1");
+    }
+
+    if (_bzobjs_adapter_is_usable_for_device(bzobj)
+        != bzobj->was_usable_adapter_for_device_before) {
+        nm_utils_strbuf_append_str(&buf, &len, prefix);
+        prefix = ", ";
+        nm_utils_strbuf_append(&buf, &len, "change-usable-adapter-for-device: 1");
+    }
+
+    return buf0;
+}
+
+#define _LOG_bzobj(bzobj, context)                           \
+    G_STMT_START                                             \
+    {                                                        \
+        const BzDBusObj *const _bzobj = (bzobj);             \
+        char                   _buf[500];                    \
+                                                             \
+        _LOGT("change %-21s %s : { %s }",                    \
+              (context),                                     \
+              _bzobj->object_path,                           \
+              _bzobj_to_string(_bzobj, _buf, sizeof(_buf))); \
+    }                                                        \
+    G_STMT_END
+
+static gboolean
+_bzobjs_is_dead(const BzDBusObj *bzobj)
+{
+    return !bzobj->d_has_adapter_iface && !bzobj->d_has_device_iface && !bzobj->d_has_network_iface
+           && !bzobj->d_has_network_server_iface && c_list_is_empty(&bzobj->process_change_lst);
+}
+
+static BzDBusObj *
+_bzobjs_get(NMBluezManager *self, const char *object_path)
+{
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->bzobjs, &object_path);
+}
+
+static BzDBusObj *
+_bzobjs_add(NMBluezManager *self, const char *object_path)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    bzobj = _bz_dbus_obj_new(self, object_path);
+    if (!g_hash_table_add(priv->bzobjs, bzobj))
+        nm_assert_not_reached();
+    return bzobj;
+}
+
+static void
+_bzobjs_del(BzDBusObj *bzobj)
+{
+    nm_assert(bzobj);
+    nm_assert(bzobj == _bzobjs_get(bzobj->self, bzobj->object_path));
+
+    if (!g_hash_table_remove(NM_BLUEZ_MANAGER_GET_PRIVATE(bzobj->self)->bzobjs, bzobj))
+        nm_assert_not_reached();
+}
+
+static void
+_bzobjs_del_if_dead(BzDBusObj *bzobj)
+{
+    if (_bzobjs_is_dead(bzobj))
+        _bzobjs_del(bzobj);
+}
+
+static BzDBusObj *
+_bzobjs_init(NMBluezManager *self, BzDBusObj **inout, const char *object_path)
+{
+    nm_assert(NM_IS_BLUEZ_MANAGER(self));
+    nm_assert(object_path);
+    nm_assert(inout);
+
+    if (!*inout) {
+        *inout = _bzobjs_get(self, object_path);
+        if (!*inout)
+            *inout = _bzobjs_add(self, object_path);
+    }
+
+    nm_assert(nm_streq((*inout)->object_path, object_path));
+    nm_assert(*inout == _bzobjs_get(self, object_path));
+    return *inout;
+}
+
+static gboolean
+_bzobjs_adapter_is_usable_for_device(const BzDBusObj *bzobj)
+{
+    return bzobj->d_has_adapter_iface && bzobj->d_adapter.address && bzobj->d_adapter_powered;
+}
+
+static gboolean
+_bzobjs_device_is_usable(const BzDBusObj *bzobj,
+                         BzDBusObj **     out_adapter_bzobj,
+                         gboolean *       out_create_panu_connection)
+{
+    NMBluezManager *       self;
+    NMBluezManagerPrivate *priv;
+    gboolean               usable_dun = FALSE;
+    gboolean               usable_nap = FALSE;
+    BzDBusObj *            bzobj_adapter;
+    gboolean               create_panu_connection = FALSE;
+
+    if (!bzobj->d_has_device_iface
+        || !NM_FLAGS_ANY((NMBluetoothCapabilities) bzobj->d_device_capabilities,
+                         _NM_BT_CAPABILITY_SUPPORTED)
+        || !bzobj->d_device.name || !bzobj->d_device.address || !bzobj->d_device_paired
+        || !bzobj->d_device.adapter)
+        goto out_unusable;
+
+    self = bzobj->self;
+
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    if (!priv->settings_registered)
+        goto out_unusable;
+
+    bzobj_adapter = _bzobjs_get(self, bzobj->d_device.adapter);
+    if (!bzobj_adapter || !_bzobjs_adapter_is_usable_for_device(bzobj_adapter))
+        goto out_unusable;
+
+#if WITH_BLUEZ5_DUN
+    if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_DUN)) {
+        if (_conn_track_find_head(self, NM_BT_CAPABILITY_DUN, bzobj->d_device.address))
+            usable_dun = TRUE;
+    }
+#endif
+
+    if (NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)) {
+        if (!bzobj->d_has_network_iface)
+            usable_nap = FALSE;
+        else if (_conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address))
+            usable_nap = TRUE;
+        else if (bzobj->x_device_panu_connection_allow_create) {
+            /* We didn't yet try to create a connection. Presume we are going to create
+             * it when the time comes... */
+            usable_nap             = TRUE;
+            create_panu_connection = TRUE;
+        }
+    }
+
+    if (!usable_dun && !usable_nap) {
+        if (bzobj->x_device.device_bt
+            && nm_device_get_state(NM_DEVICE(bzobj->x_device.device_bt))
+                   > NM_DEVICE_STATE_DISCONNECTED) {
+            /* The device is still activated... the absence of a profile does not
+             * render it unusable (yet). But since there is no more profile, the
+             * device is probably about to disconnect. */
+        } else
+            goto out_unusable;
+    }
+
+    NM_SET_OUT(out_create_panu_connection, create_panu_connection);
+    NM_SET_OUT(out_adapter_bzobj, bzobj_adapter);
+    return TRUE;
+
+out_unusable:
+    NM_SET_OUT(out_create_panu_connection, FALSE);
+    NM_SET_OUT(out_adapter_bzobj, NULL);
+    return FALSE;
+}
+
+static gboolean
+_bzobjs_device_is_connected(const BzDBusObj *bzobj)
+{
+    nm_assert(_bzobjs_device_is_usable(bzobj, NULL, NULL));
+
+    if (!bzobj->d_has_device_iface || !bzobj->d_device_connected)
+        return FALSE;
+
+    if (bzobj->d_has_network_iface && bzobj->d_network_connected)
+        return TRUE;
+    if (bzobj->x_device.connect_dun_context) {
+        /* As long as we have a dun-context, we consider it connected.
+         *
+         * We require NMDeviceBt to try to connect to the modem, and if that fails,
+         * it will disconnect. */
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+_bzobjs_network_server_is_usable(const BzDBusObj *bzobj, gboolean require_powered)
+{
+    return bzobj->d_has_network_server_iface && bzobj->d_has_adapter_iface
+           && bzobj->d_adapter.address && (!require_powered || bzobj->d_adapter_powered);
+}
+
+/*****************************************************************************/
+
+static ConnDataHead *
+_conn_data_head_new(NMBluetoothCapabilities bt_type, const char *bdaddr)
+{
+    ConnDataHead *cdata_hd;
+    gsize         l;
+
+    nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP));
+    nm_assert(bdaddr);
+
+    l         = strlen(bdaddr) + 1;
+    cdata_hd  = g_malloc(sizeof(ConnDataHead) + l);
+    *cdata_hd = (ConnDataHead){
+        .bdaddr   = cdata_hd->bdaddr_data,
+        .lst_head = C_LIST_INIT(cdata_hd->lst_head),
+        .bt_type  = bt_type,
+    };
+    memcpy(cdata_hd->bdaddr_data, bdaddr, l);
+
+    nm_assert(cdata_hd->bt_type == bt_type);
+
+    return cdata_hd;
+}
+
+static guint
+_conn_data_head_hash(gconstpointer ptr)
+{
+    const ConnDataHead *cdata_hd = ptr;
+    NMHashState         h;
+
+    nm_hash_init(&h, 520317467u);
+    nm_hash_update_val(&h, (NMBluetoothCapabilities) cdata_hd->bt_type);
+    nm_hash_update_str(&h, cdata_hd->bdaddr);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_conn_data_head_equal(gconstpointer a, gconstpointer b)
+{
+    const ConnDataHead *cdata_hd_a = a;
+    const ConnDataHead *cdata_hd_b = b;
+
+    return cdata_hd_a->bt_type == cdata_hd_b->bt_type
+           && nm_streq(cdata_hd_a->bdaddr, cdata_hd_b->bdaddr);
+}
+
+static ConnDataHead *
+_conn_track_find_head(NMBluezManager *self, NMBluetoothCapabilities bt_type, const char *bdaddr)
+{
+    ConnDataHead cdata_hd = {
+        .bt_type = bt_type,
+        .bdaddr  = bdaddr,
+    };
+
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_heads, &cdata_hd);
+}
+
+static ConnDataElem *
+_conn_track_find_elem(NMBluezManager *self, NMSettingsConnection *sett_conn)
+{
+    G_STATIC_ASSERT(G_STRUCT_OFFSET(ConnDataElem, sett_conn) == 0);
+
+    return g_hash_table_lookup(NM_BLUEZ_MANAGER_GET_PRIVATE(self)->conn_data_elems, &sett_conn);
+}
+
+static gboolean
+_conn_track_is_relevant_connection(NMConnection *           connection,
+                                   NMBluetoothCapabilities *out_bt_type,
+                                   const char **            out_bdaddr)
+{
+    NMSettingBluetooth *    s_bt;
+    NMBluetoothCapabilities bt_type;
+    const char *            bdaddr;
+    const char *            b_type;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+    if (!s_bt)
+        return FALSE;
+
+    if (!nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME))
+        return FALSE;
+
+    bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (!bdaddr)
+        return FALSE;
+
+    b_type = nm_setting_bluetooth_get_connection_type(s_bt);
+
+    if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_DUN))
+        bt_type = NM_BT_CAPABILITY_DUN;
+    else if (nm_streq(b_type, NM_SETTING_BLUETOOTH_TYPE_PANU))
+        bt_type = NM_BT_CAPABILITY_NAP;
+    else
+        return FALSE;
+
+    NM_SET_OUT(out_bt_type, bt_type);
+    NM_SET_OUT(out_bdaddr, bdaddr);
+    return TRUE;
+}
+
+static gboolean
+_conn_track_is_relevant_sett_conn(NMSettingsConnection *   sett_conn,
+                                  NMBluetoothCapabilities *out_bt_type,
+                                  const char **            out_bdaddr)
+{
+    NMConnection *connection;
+
+    connection = nm_settings_connection_get_connection(sett_conn);
+    if (!connection)
+        return FALSE;
+
+    return _conn_track_is_relevant_connection(connection, out_bt_type, out_bdaddr);
+}
+
+static gboolean
+_conn_track_is_relevant_for_sett_conn(NMSettingsConnection *  sett_conn,
+                                      NMBluetoothCapabilities bt_type,
+                                      const char *            bdaddr)
+{
+    NMBluetoothCapabilities x_bt_type;
+    const char *            x_bdaddr;
+
+    return bdaddr && _conn_track_is_relevant_sett_conn(sett_conn, &x_bt_type, &x_bdaddr)
+           && x_bt_type == bt_type && nm_streq(x_bdaddr, bdaddr);
+}
+
+static void
+_conn_track_schedule_notify(NMBluezManager *        self,
+                            NMBluetoothCapabilities bt_type,
+                            const char *            bdaddr)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    GHashTableIter         iter;
+    BzDBusObj *            bzobj;
+
+    g_hash_table_iter_init(&iter, priv->bzobjs);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+        gboolean device_is_usable;
+
+        device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+        if (bzobj->x_device_is_usable != device_is_usable)
+            _process_change_idle_schedule(self, bzobj);
+    }
+}
+
+static void
+_conn_track_update(NMBluezManager *      self,
+                   NMSettingsConnection *sett_conn,
+                   gboolean              track,
+                   gboolean *            out_changed,
+                   gboolean *            out_changed_usable,
+                   ConnDataElem **       out_conn_data_elem)
+{
+    NMBluezManagerPrivate * priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    ConnDataHead *          cdata_hd;
+    ConnDataElem *          cdata_el;
+    ConnDataElem *          cdata_el_remove = NULL;
+    NMBluetoothCapabilities bt_type;
+    const char *            bdaddr;
+    gboolean                changed        = FALSE;
+    gboolean                changed_usable = FALSE;
+    char                    sbuf_cap[100];
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    cdata_el = _conn_track_find_elem(self, sett_conn);
+
+    if (track)
+        track = _conn_track_is_relevant_sett_conn(sett_conn, &bt_type, &bdaddr);
+
+    if (!track) {
+        cdata_el_remove = g_steal_pointer(&cdata_el);
+        goto out_remove;
+    }
+
+    if (cdata_el) {
+        cdata_hd = cdata_el->cdata_hd;
+        if (cdata_hd->bt_type != bt_type || !nm_streq(cdata_hd->bdaddr, bdaddr))
+            cdata_el_remove = g_steal_pointer(&cdata_el);
+    }
+
+    if (!cdata_el) {
+        _LOGT("connection: track for %s, %s: %s (%s)",
+              nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)),
+              bdaddr,
+              nm_settings_connection_get_uuid(sett_conn),
+              nm_settings_connection_get_id(sett_conn));
+        changed  = TRUE;
+        cdata_hd = _conn_track_find_head(self, bt_type, bdaddr);
+        if (!cdata_hd) {
+            changed_usable = TRUE;
+            cdata_hd       = _conn_data_head_new(bt_type, bdaddr);
+            if (!g_hash_table_add(priv->conn_data_heads, cdata_hd))
+                nm_assert_not_reached();
+            _conn_track_schedule_notify(self, bt_type, bdaddr);
+        }
+        cdata_el            = g_slice_new(ConnDataElem);
+        cdata_el->sett_conn = sett_conn;
+        cdata_el->cdata_hd  = cdata_hd;
+        c_list_link_tail(&cdata_hd->lst_head, &cdata_el->lst);
+        if (!g_hash_table_add(priv->conn_data_elems, cdata_el))
+            nm_assert_not_reached();
+    }
+
+out_remove:
+    if (cdata_el_remove) {
+        GHashTableIter iter;
+        BzDBusObj *    bzobj;
+
+        _LOGT("connection: untrack for %s, %s: %s (%s)",
+              nm_bluetooth_capability_to_string(cdata_el_remove->cdata_hd->bt_type,
+                                                sbuf_cap,
+                                                sizeof(sbuf_cap)),
+              cdata_el_remove->cdata_hd->bdaddr,
+              nm_settings_connection_get_uuid(sett_conn),
+              nm_settings_connection_get_id(sett_conn));
+
+        g_hash_table_iter_init(&iter, priv->bzobjs);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+            if (bzobj->x_device.panu_connection == sett_conn)
+                bzobj->x_device.panu_connection = NULL;
+        }
+
+        changed  = TRUE;
+        cdata_hd = cdata_el_remove->cdata_hd;
+        c_list_unlink_stale(&cdata_el_remove->lst);
+        if (!g_hash_table_remove(priv->conn_data_elems, cdata_el_remove))
+            nm_assert_not_reached();
+        if (c_list_is_empty(&cdata_hd->lst_head)) {
+            changed_usable = TRUE;
+            _conn_track_schedule_notify(self, cdata_hd->bt_type, cdata_hd->bdaddr);
+            if (!g_hash_table_remove(priv->conn_data_heads, cdata_hd))
+                nm_assert_not_reached();
+        }
+    }
+
+    NM_SET_OUT(out_changed, changed);
+    NM_SET_OUT(out_changed_usable, changed_usable);
+    NM_SET_OUT(out_conn_data_elem, cdata_el);
+}
+
+/*****************************************************************************/
+
+static void
+cp_connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self)
+{
+    _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL);
+}
+
+static void
+cp_connection_updated(NMSettings *          settings,
+                      NMSettingsConnection *sett_conn,
+                      guint                 update_reason_u,
+                      NMBluezManager *      self)
+{
+    _conn_track_update(self, sett_conn, TRUE, NULL, NULL, NULL);
+}
+
+static void
+cp_connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, NMBluezManager *self)
+{
+    _conn_track_update(self, sett_conn, FALSE, NULL, NULL, NULL);
+}
+
+/*****************************************************************************/
+
+static NMBluezManager *
+_network_server_get_bluez_manager(const NMBtVTableNetworkServer *vtable_network_server)
+{
+    NMBluezManager *self;
+
+    self = (NMBluezManager *) (((char *) vtable_network_server)
+                               - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server));
+
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), NULL);
+
+    return self;
+}
+
+static BzDBusObj *
+_network_server_find_has_device(NMBluezManagerPrivate *priv, NMDevice *device)
+{
+    BzDBusObj *bzobj;
+
+    c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+        if (bzobj->x_network_server.device_br == device)
+            return bzobj;
+    }
+    return NULL;
+}
+
+static BzDBusObj *
+_network_server_find_available(NMBluezManagerPrivate *priv,
+                               const char *           addr,
+                               NMDevice *             device_accept_busy)
+{
+    BzDBusObj *bzobj;
+
+    c_list_for_each_entry (bzobj, &priv->network_server_lst_head, x_network_server.lst) {
+        if (bzobj->x_network_server.device_br) {
+            if (bzobj->x_network_server.device_br != device_accept_busy)
+                continue;
+        }
+        if (addr && !nm_streq(addr, bzobj->d_adapter.address))
+            continue;
+        nm_assert(!bzobj->x_network_server.r_req_data);
+        return bzobj;
+    }
+    return NULL;
+}
+
+static gboolean
+_network_server_vt_is_available(const NMBtVTableNetworkServer *vtable,
+                                const char *                   addr,
+                                NMDevice *                     device_accept_busy)
+{
+    NMBluezManager *       self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    return !!_network_server_find_available(priv, addr, device_accept_busy);
+}
+
+static void
+_network_server_register_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret = NULL;
+    gs_free_error GError *error    = NULL;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (!ret) {
+        _LOGT("NAP: [%s]: registering failed: %s", bzobj->object_path, error->message);
+    } else
+        _LOGT("NAP: [%s]: registration successful", bzobj->object_path);
+
+    g_clear_object(&bzobj->x_network_server.r_req_data->int_cancellable);
+    _network_server_register_req_data_complete(g_steal_pointer(&bzobj->x_network_server.r_req_data),
+                                               error);
+}
+
+static void
+_network_server_register_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj)
+{
+    _network_server_unregister_bridge(bzobj->self, bzobj, "registration cancelled");
+}
+
+static gboolean
+_network_server_vt_register_bridge(const NMBtVTableNetworkServer *vtable,
+                                   const char *                   addr,
+                                   NMDevice *                     device,
+                                   GCancellable *                 cancellable,
+                                   NMBtVTableRegisterCallback     callback,
+                                   gpointer                       callback_user_data,
+                                   GError **                      error)
+{
+    NMBluezManager *              self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *       priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    NetworkServerRegisterReqData *r_req_data;
+    BzDBusObj *                   bzobj;
+    const char *                  ifname;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+    g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE);
+
+    nm_assert(!g_cancellable_is_cancelled(cancellable));
+    nm_assert(!_network_server_find_has_device(priv, device));
+
+    ifname = nm_device_get_iface(device);
+    g_return_val_if_fail(ifname, FALSE);
+
+    g_return_val_if_fail(ifname, FALSE);
+
+    bzobj = _network_server_find_available(priv, addr, NULL);
+    if (!bzobj) {
+        /* The device checked that a network server is available, before
+         * starting the activation, but for some reason it no longer is.
+         * Indicate that the activation should not proceed. */
+        if (addr) {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               "adapter %s is not available for %s",
+                               addr,
+                               ifname);
+        } else {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               "no adapter available for %s",
+                               ifname);
+        }
+        return FALSE;
+    }
+
+    _LOGD("NAP: [%s]: registering \"%s\" on adapter %s",
+          bzobj->object_path,
+          ifname,
+          bzobj->d_adapter.address);
+
+    r_req_data  = g_slice_new(NetworkServerRegisterReqData);
+    *r_req_data = (NetworkServerRegisterReqData){
+        .int_cancellable    = g_cancellable_new(),
+        .ext_cancellable    = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+        .ext_cancelled_id   = g_signal_connect(cancellable,
+                                             "cancelled",
+                                             G_CALLBACK(_network_server_register_cancelled_cb),
+                                             bzobj),
+    };
+
+    bzobj->x_network_server.device_br  = g_object_ref(device);
+    bzobj->x_network_server.r_req_data = r_req_data;
+
+    g_dbus_connection_call(priv->dbus_connection,
+                           priv->name_owner,
+                           bzobj->object_path,
+                           NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+                           "Register",
+                           g_variant_new("(ss)", BLUETOOTH_CONNECT_NAP, ifname),
+                           NULL,
+                           G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                           -1,
+                           bzobj->x_network_server.r_req_data->int_cancellable,
+                           _network_server_register_cb,
+                           bzobj);
+    return TRUE;
+}
+
+static void
+_network_server_unregister_bridge_complete_on_idle_cb(gpointer user_data, GCancellable *cancellable)
+{
+    gs_free_error GError *        error  = NULL;
+    gs_free char *                reason = NULL;
+    NetworkServerRegisterReqData *r_req_data;
+
+    nm_utils_user_data_unpack(user_data, &r_req_data, &reason);
+
+    nm_utils_error_set(&error,
+                       NM_UTILS_ERROR_UNKNOWN,
+                       "registration was aborted due to %s",
+                       reason);
+    _network_server_register_req_data_complete(r_req_data, error);
+}
+
+static void
+_network_server_unregister_bridge(NMBluezManager *self, BzDBusObj *bzobj, const char *reason)
+{
+    NMBluezManagerPrivate *    priv             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    _nm_unused gs_unref_object NMDevice *device = NULL;
+    NetworkServerRegisterReqData *       r_req_data;
+
+    nm_assert(NM_IS_DEVICE(bzobj->x_network_server.device_br));
+
+    _LOGD("NAP: [%s]: unregistering \"%s\" (%s)",
+          bzobj->object_path,
+          nm_device_get_iface(bzobj->x_network_server.device_br),
+          reason);
+
+    device = g_steal_pointer(&bzobj->x_network_server.device_br);
+
+    r_req_data = g_steal_pointer(&bzobj->x_network_server.r_req_data);
+
+    if (priv->name_owner) {
+        gs_unref_object GCancellable *cancellable = NULL;
+
+        cancellable = g_cancellable_new();
+
+        nm_shutdown_wait_obj_register_cancellable_full(
+            cancellable,
+            g_strdup_printf("bt-unregister-nap[%s]", bzobj->object_path),
+            TRUE);
+
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_NETWORK_SERVER_INTERFACE,
+                               "Unregister",
+                               g_variant_new("(s)", BLUETOOTH_CONNECT_NAP),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               -1,
+                               cancellable,
+                               _dbus_call_complete_cb_nop,
+                               NULL);
+    }
+
+    if (r_req_data) {
+        nm_clear_g_cancellable(&r_req_data->int_cancellable);
+        nm_utils_invoke_on_idle(r_req_data->ext_cancellable,
+                                _network_server_unregister_bridge_complete_on_idle_cb,
+                                nm_utils_user_data_pack(r_req_data, g_strdup(reason)));
+    }
+
+    _nm_device_bridge_notify_unregister_bt_nap(device, reason);
+}
+
+static gboolean
+_network_server_vt_unregister_bridge(const NMBtVTableNetworkServer *vtable, NMDevice *device)
+{
+    NMBluezManager *       self = _network_server_get_bluez_manager(vtable);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
+
+    bzobj = _network_server_find_has_device(priv, device);
+    if (bzobj)
+        _network_server_unregister_bridge(self, bzobj, "disconnecting");
+
+    return TRUE;
+}
+
+static void
+_network_server_process_change(BzDBusObj *bzobj, gboolean *out_emit_device_availability_changed)
+{
+    NMBluezManager *       self = bzobj->self;
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               network_server_is_usable;
+    gboolean               emit_device_availability_changed = FALSE;
+
+    network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+    if (!network_server_is_usable) {
+        if (!c_list_is_empty(&bzobj->x_network_server.lst)) {
+            emit_device_availability_changed = TRUE;
+            c_list_unlink(&bzobj->x_network_server.lst);
+        }
+
+        nm_clear_g_free(&bzobj->x_network_server.adapter_address);
+
+        if (bzobj->x_network_server.device_br) {
+            _network_server_unregister_bridge(self,
+                                              bzobj,
+                                              _bzobjs_network_server_is_usable(bzobj, FALSE)
+                                                  ? "adapter disabled"
+                                                  : "adapter disappeared");
+        }
+
+    } else {
+        if (!nm_streq0(bzobj->x_network_server.adapter_address, bzobj->d_adapter.address)) {
+            emit_device_availability_changed = TRUE;
+            g_free(bzobj->x_network_server.adapter_address);
+            bzobj->x_network_server.adapter_address = g_strdup(bzobj->d_adapter.address);
+        }
+
+        if (c_list_is_empty(&bzobj->x_network_server.lst)) {
+            emit_device_availability_changed = TRUE;
+            c_list_link_tail(&priv->network_server_lst_head, &bzobj->x_network_server.lst);
+        }
+    }
+
+    if (emit_device_availability_changed)
+        NM_SET_OUT(out_emit_device_availability_changed, TRUE);
+}
+
+/*****************************************************************************/
+
+static void
+_conn_create_panu_connection(NMBluezManager *self, BzDBusObj *bzobj)
+{
+    NMBluezManagerPrivate *priv              = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingsConnection *        added;
+    NMSetting *                   setting;
+    gs_free char *                id = NULL;
+    char                          uuid[37];
+    gs_free_error GError *error = NULL;
+
+    nm_utils_uuid_generate_buf(uuid);
+    id = g_strdup_printf(_("%s Network"), bzobj->d_device.name);
+
+    connection = nm_simple_connection_new();
+
+    setting = nm_setting_connection_new();
+    g_object_set(setting,
+                 NM_SETTING_CONNECTION_ID,
+                 id,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 FALSE,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_BLUETOOTH_SETTING_NAME,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    setting = nm_setting_bluetooth_new();
+    g_object_set(setting,
+                 NM_SETTING_BLUETOOTH_BDADDR,
+                 bzobj->d_device.address,
+                 NM_SETTING_BLUETOOTH_TYPE,
+                 NM_SETTING_BLUETOOTH_TYPE_PANU,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
+        _LOGE("connection: couldn't generate a connection for NAP device: %s", error->message);
+        g_return_if_reached();
+    }
+
+    nm_assert(_conn_track_is_relevant_connection(connection, NULL, NULL));
+
+    _LOGT("connection: create in-memory PANU connection %s (%s) for device \"%s\" (%s)",
+          uuid,
+          id,
+          bzobj->d_device.name,
+          bzobj->d_device.address);
+
+    nm_settings_add_connection(priv->settings,
+                               connection,
+                               NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                               NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                               NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                               &added,
+                               &error);
+    if (!added) {
+        _LOGW("connection: couldn't add new Bluetooth connection for NAP device: '%s' (%s): %s",
+              id,
+              uuid,
+              error->message);
+        return;
+    }
+
+    if (!_conn_track_is_relevant_for_sett_conn(added, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)
+        || !_conn_track_find_elem(self, added) || bzobj->x_device.panu_connection) {
+        _LOGE("connection: something went wrong creating PANU connection %s (%s) for device '%s'",
+              uuid,
+              id,
+              bzobj->d_device.address);
+        g_return_if_reached();
+    }
+
+    bzobj->x_device.panu_connection = added;
+}
+
+/*****************************************************************************/
+
+static void
+_device_state_changed_cb(NMDevice *device,
+                         guint     new_state_u,
+                         guint     old_state_u,
+                         guint     reason_u,
+                         gpointer  user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) {
+        /* the device got unusable? Need to revisit it... */
+        _process_change_idle_schedule(bzobj->self, bzobj);
+    }
+}
+
+static void
+_device_process_change(BzDBusObj *bzobj)
+{
+    NMBluezManager *self                       = bzobj->self;
+    gs_unref_object NMDeviceBt *device_added   = NULL;
+    gs_unref_object NMDeviceBt *device_deleted = NULL;
+    gboolean                    device_is_usable;
+    gboolean                    create_panu_connection = FALSE;
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, &create_panu_connection);
+
+    if (create_panu_connection) {
+        bzobj->x_device_panu_connection_allow_create = FALSE;
+        _conn_create_panu_connection(self, bzobj);
+        device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+    } else {
+        if (device_is_usable && bzobj->x_device_panu_connection_allow_create
+            && NM_FLAGS_HAS(bzobj->d_device_capabilities, NM_BT_CAPABILITY_NAP)
+            && _conn_track_find_head(self, NM_BT_CAPABILITY_NAP, bzobj->d_device.address)) {
+            /* We have a useable device and also a panu-connection. We block future attemps
+             * to generate a connection. */
+            bzobj->x_device_panu_connection_allow_create = FALSE;
+        }
+        if (bzobj->x_device.panu_connection) {
+            if (!NM_FLAGS_HAS(nm_settings_connection_get_flags(bzobj->x_device.panu_connection),
+                              NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+                /* the connection that we generated earlier still exists, but it's not longer the same
+                 * as it was when we created it. Forget about it, so that we don't delete the profile later... */
+                bzobj->x_device.panu_connection = NULL;
+            } else {
+                if (!device_is_usable
+                    || !_conn_track_is_relevant_for_sett_conn(bzobj->x_device.panu_connection,
+                                                              NM_BT_CAPABILITY_NAP,
+                                                              bzobj->d_device.address)) {
+                    _LOGT("connection: delete in-memory PANU connection %s (%s) as device %s",
+                          nm_settings_connection_get_uuid(bzobj->x_device.panu_connection),
+                          nm_settings_connection_get_id(bzobj->x_device.panu_connection),
+                          !device_is_usable ? "is now unusable" : "no longer matches");
+                    bzobj->x_device_panu_connection_allow_create = TRUE;
+                    nm_settings_connection_delete(g_steal_pointer(&bzobj->x_device.panu_connection),
+                                                  FALSE);
+                }
+            }
+        }
+    }
+
+    bzobj->x_device_is_connected = device_is_usable && _bzobjs_device_is_connected(bzobj);
+
+    bzobj->x_device_is_usable = device_is_usable;
+
+    if (bzobj->x_device.device_bt) {
+        const char *device_to_delete_msg;
+
+        if (!device_is_usable)
+            device_to_delete_msg = "device became unusable";
+        else if (!_nm_device_bt_for_same_device(bzobj->x_device.device_bt,
+                                                bzobj->object_path,
+                                                bzobj->d_device.address,
+                                                NULL,
+                                                bzobj->d_device_capabilities))
+            device_to_delete_msg = "device is no longer compatible";
+        else
+            device_to_delete_msg = NULL;
+
+        if (device_to_delete_msg) {
+            nm_clear_g_signal_handler(bzobj->x_device.device_bt,
+                                      &bzobj->x_device.device_bt_signal_id);
+
+            device_deleted = g_steal_pointer(&bzobj->x_device.device_bt);
+
+            _LOGD("[%s]: drop device because %s", bzobj->object_path, device_to_delete_msg);
+
+            _connect_disconnect(self, bzobj, device_to_delete_msg);
+        }
+    }
+
+    if (device_is_usable) {
+        if (!bzobj->x_device.device_bt) {
+            bzobj->x_device.device_bt = nm_device_bt_new(self,
+                                                         bzobj->object_path,
+                                                         bzobj->d_device.address,
+                                                         bzobj->d_device.name,
+                                                         bzobj->d_device_capabilities);
+            device_added              = g_object_ref(bzobj->x_device.device_bt);
+            bzobj->x_device.device_bt_signal_id =
+                g_signal_connect(device_added,
+                                 NM_DEVICE_STATE_CHANGED,
+                                 G_CALLBACK(_device_state_changed_cb),
+                                 bzobj);
+        } else
+            _nm_device_bt_notify_set_name(bzobj->x_device.device_bt, bzobj->d_device.name);
+
+        _nm_device_bt_notify_set_connected(bzobj->x_device.device_bt, bzobj->x_device_is_connected);
+    }
+
+    if (bzobj->x_device.c_req_data && !bzobj->x_device.c_req_data->int_cancellable
+        && bzobj->x_device_is_connected) {
+        gs_free char *device_name = g_steal_pointer(&bzobj->x_device.c_req_data->device_name);
+
+        _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data),
+                                          self,
+                                          device_name,
+                                          NULL);
+    }
+
+    if (device_added)
+        g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device_added);
+
+    if (device_deleted)
+        _nm_device_bt_notify_removed(device_deleted);
+}
+
+/*****************************************************************************/
+
+static void
+_process_change_idle_all(NMBluezManager *self, gboolean *out_emit_device_availability_changed)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj;
+
+    while (
+        (bzobj =
+             c_list_first_entry(&priv->process_change_lst_head, BzDBusObj, process_change_lst))) {
+        c_list_unlink(&bzobj->process_change_lst);
+
+        _LOG_bzobj(bzobj, "before-processing");
+
+        _device_process_change(bzobj);
+
+        _network_server_process_change(bzobj, out_emit_device_availability_changed);
+
+        _LOG_bzobj(bzobj, "after-processing");
+
+        _bzobjs_del_if_dead(bzobj);
+    }
+
+    nm_clear_g_source(&priv->process_change_idle_id);
+}
+
+static gboolean
+_process_change_idle_cb(gpointer user_data)
+{
+    NMBluezManager *       self                             = user_data;
+    NMBluezManagerPrivate *priv                             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               emit_device_availability_changed = FALSE;
+
+    _process_change_idle_all(self, &emit_device_availability_changed);
+
+    if (emit_device_availability_changed)
+        nm_manager_notify_device_availability_maybe_changed(priv->manager);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_process_change_idle_schedule(NMBluezManager *self, BzDBusObj *bzobj)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst);
+    if (priv->process_change_idle_id == 0)
+        priv->process_change_idle_id =
+            g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1, _process_change_idle_cb, self, NULL);
+}
+
+static void
+_dbus_process_changes(NMBluezManager *self, BzDBusObj *bzobj, const char *log_reason)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               network_server_is_usable;
+    gboolean               adapter_is_usable_for_device;
+    gboolean               device_is_usable;
+    gboolean               changes                     = FALSE;
+    gboolean               recheck_devices_for_adapter = FALSE;
+
+    nm_assert(bzobj);
+
+    _LOG_bzobj(bzobj, log_reason);
+
+    device_is_usable = _bzobjs_device_is_usable(bzobj, NULL, NULL);
+
+    if (bzobj->x_device_is_usable != device_is_usable)
+        changes = TRUE;
+    else if (bzobj->x_device.device_bt) {
+        if (!device_is_usable)
+            changes = TRUE;
+        else {
+            if (bzobj->x_device_is_connected != _bzobjs_device_is_connected(bzobj)
+                || !_nm_device_bt_for_same_device(bzobj->x_device.device_bt,
+                                                  bzobj->object_path,
+                                                  bzobj->d_device.address,
+                                                  bzobj->d_device.name,
+                                                  bzobj->d_device_capabilities))
+                changes = TRUE;
+        }
+    }
+
+    adapter_is_usable_for_device = _bzobjs_adapter_is_usable_for_device(bzobj);
+    if (adapter_is_usable_for_device != bzobj->was_usable_adapter_for_device_before) {
+        /* this function does not modify bzobj in any other cases except here.
+         * Usually changes are processed delayed, in the idle handler.
+         *
+         * But the bzobj->was_usable_adapter_for_device_before only exists to know whether
+         * we need to re-check device availability. It is correct to set the flag
+         * here, right before we checked. */
+        bzobj->was_usable_adapter_for_device_before = adapter_is_usable_for_device;
+        recheck_devices_for_adapter                 = TRUE;
+        changes                                     = TRUE;
+    }
+
+    if (!changes) {
+        network_server_is_usable = _bzobjs_network_server_is_usable(bzobj, TRUE);
+
+        if (network_server_is_usable != (!c_list_is_empty(&bzobj->x_network_server.lst)))
+            changes = TRUE;
+        else if (bzobj->x_network_server.device_br && !network_server_is_usable)
+            changes = TRUE;
+        else if (!nm_streq0(bzobj->d_has_adapter_iface ? bzobj->d_adapter.address : NULL,
+                            bzobj->x_network_server.adapter_address))
+            changes = TRUE;
+    }
+
+    if (changes)
+        _process_change_idle_schedule(self, bzobj);
+
+    if (recheck_devices_for_adapter) {
+        GHashTableIter iter;
+        BzDBusObj *    bzobj2;
+
+        /* we got a change to the availability of an adapter. We might need to recheck
+         * all devices that use this adapter... */
+        g_hash_table_iter_init(&iter, priv->bzobjs);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj2, NULL)) {
+            if (bzobj2 == bzobj)
+                continue;
+            if (!nm_streq0(bzobj2->d_device.adapter, bzobj->object_path))
+                continue;
+            if (c_list_is_empty(&bzobj2->process_change_lst))
+                _dbus_process_changes(self, bzobj2, "adapter-changed");
+            else
+                nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj2->process_change_lst);
+        }
+    }
+
+    _bzobjs_del_if_dead(bzobj);
+}
+
+/*****************************************************************************/
+
+#define ALL_RELEVANT_INTERFACE_NAMES          \
+    NM_MAKE_STRV(NM_BLUEZ5_ADAPTER_INTERFACE, \
+                 NM_BLUEZ5_DEVICE_INTERFACE,  \
+                 NM_BLUEZ5_NETWORK_INTERFACE, \
+                 NM_BLUEZ5_NETWORK_SERVER_INTERFACE)
+
+static gboolean
+_dbus_handle_properties_changed(NMBluezManager *   self,
+                                const char *       object_path,
+                                const char *       interface_name,
+                                GVariant *         changed_properties,
+                                const char *const *invalidated_properties,
+                                BzDBusObj **       inout_bzobj)
+{
+    BzDBusObj *  bzobj   = NULL;
+    gboolean     changed = FALSE;
+    const char * property_name;
+    GVariant *   property_value;
+    GVariantIter iter_prop;
+    gsize        i;
+
+    if (!invalidated_properties)
+        invalidated_properties = NM_PTRARRAY_EMPTY(const char *);
+
+    nm_assert(g_variant_is_of_type(changed_properties, G_VARIANT_TYPE("a{sv}")));
+
+    if (inout_bzobj) {
+        bzobj = *inout_bzobj;
+        nm_assert(!bzobj || nm_streq(object_path, bzobj->object_path));
+    }
+
+    if (changed_properties)
+        g_variant_iter_init(&iter_prop, changed_properties);
+
+    if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_adapter_iface) {
+            changed                    = TRUE;
+            bzobj->d_has_adapter_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Address")) {
+                gs_free char *s =
+                    g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                        ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL),
+                                                    ETH_ALEN)
+                        : NULL;
+
+                if (!nm_streq0(bzobj->d_adapter.address, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_adapter.address);
+                    bzobj->d_adapter.address = g_steal_pointer(&s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Powered")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_adapter_powered != v) {
+                    changed                  = TRUE;
+                    bzobj->d_adapter_powered = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Address")) {
+                if (bzobj->d_adapter.address) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_adapter.address);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Powered")) {
+                if (bzobj->d_adapter_powered) {
+                    changed                  = TRUE;
+                    bzobj->d_adapter_powered = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_device_iface) {
+            changed                   = TRUE;
+            bzobj->d_has_device_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Address")) {
+                gs_free char *s =
+                    g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                        ? nm_utils_hwaddr_canonical(g_variant_get_string(property_value, NULL),
+                                                    ETH_ALEN)
+                        : NULL;
+
+                if (!nm_streq0(bzobj->d_device.address, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.address);
+                    bzobj->d_device.address = g_steal_pointer(&s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Name")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_device.name, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.name);
+                    bzobj->d_device.name = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Adapter")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_OBJECT_PATH)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_device.adapter, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.adapter);
+                    bzobj->d_device.adapter = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "UUIDs")) {
+                NMBluetoothCapabilities capabilities = NM_BT_CAPABILITY_NONE;
+
+                if (g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING_ARRAY)) {
+                    gs_free const char **s = g_variant_get_strv(property_value, NULL);
+
+                    capabilities = convert_uuids_to_capabilities(s);
+                }
+                if (bzobj->d_device_capabilities != capabilities) {
+                    changed                      = TRUE;
+                    bzobj->d_device_capabilities = capabilities;
+                    nm_assert(bzobj->d_device_capabilities == capabilities);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_device_connected != v) {
+                    changed                   = TRUE;
+                    bzobj->d_device_connected = v;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Paired")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_device_paired != v) {
+                    changed                = TRUE;
+                    bzobj->d_device_paired = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Address")) {
+                if (bzobj->d_device.address) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.address);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Name")) {
+                if (bzobj->d_device.name) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.name);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Adapter")) {
+                if (bzobj->d_device.adapter) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_device.adapter);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "UUIDs")) {
+                if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+                    changed                      = TRUE;
+                    bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                if (bzobj->d_device_connected) {
+                    changed                   = TRUE;
+                    bzobj->d_device_connected = FALSE;
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Paired")) {
+                if (bzobj->d_device_paired) {
+                    changed                = TRUE;
+                    bzobj->d_device_paired = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_network_iface) {
+            changed                    = TRUE;
+            bzobj->d_has_network_iface = TRUE;
+        }
+
+        while (changed_properties
+               && g_variant_iter_next(&iter_prop, "{&sv}", &property_name, &property_value)) {
+            _nm_unused gs_unref_variant GVariant *property_value_free = property_value;
+
+            if (nm_streq(property_name, "Interface")) {
+                const char *s = g_variant_is_of_type(property_value, G_VARIANT_TYPE_STRING)
+                                    ? g_variant_get_string(property_value, NULL)
+                                    : NULL;
+
+                if (!nm_streq0(bzobj->d_network.interface, s)) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_network.interface);
+                    bzobj->d_network.interface = g_strdup(s);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                bool v = g_variant_is_of_type(property_value, G_VARIANT_TYPE_BOOLEAN)
+                         && g_variant_get_boolean(property_value);
+
+                if (bzobj->d_network_connected != v) {
+                    changed                    = TRUE;
+                    bzobj->d_network_connected = v;
+                }
+                continue;
+            }
+        }
+
+        for (i = 0; (property_name = invalidated_properties[i]); i++) {
+            if (nm_streq(property_name, "Interface")) {
+                if (bzobj->d_network.interface) {
+                    changed = TRUE;
+                    nm_clear_g_free(&bzobj->d_network.interface);
+                }
+                continue;
+            }
+            if (nm_streq(property_name, "Connected")) {
+                if (bzobj->d_network_connected) {
+                    changed                    = TRUE;
+                    bzobj->d_network_connected = FALSE;
+                }
+                continue;
+            }
+        }
+
+    } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+        _bzobjs_init(self, &bzobj, object_path);
+        if (!bzobj->d_has_network_server_iface) {
+            changed                           = TRUE;
+            bzobj->d_has_network_server_iface = TRUE;
+        }
+    }
+
+    nm_assert(!changed || bzobj);
+
+    if (inout_bzobj)
+        *inout_bzobj = bzobj;
+
+    return changed;
+}
+
+static void
+_dbus_handle_interface_added(NMBluezManager *self,
+                             const char *    object_path,
+                             GVariant *      ifaces,
+                             gboolean        initial_get_managed_objects)
+{
+    BzDBusObj *  bzobj   = NULL;
+    gboolean     changed = FALSE;
+    const char * interface_name;
+    GVariant *   changed_properties;
+    GVariantIter iter_ifaces;
+
+    nm_assert(g_variant_is_of_type(ifaces, G_VARIANT_TYPE("a{sa{sv}}")));
+
+    g_variant_iter_init(&iter_ifaces, ifaces);
+    while (g_variant_iter_next(&iter_ifaces, "{&s@a{sv}}", &interface_name, &changed_properties)) {
+        _nm_unused gs_unref_variant GVariant *changed_properties_free = changed_properties;
+
+        if (_dbus_handle_properties_changed(self,
+                                            object_path,
+                                            interface_name,
+                                            changed_properties,
+                                            NULL,
+                                            &bzobj))
+            changed = TRUE;
+    }
+
+    if (changed) {
+        _dbus_process_changes(self,
+                              bzobj,
+                              initial_get_managed_objects ? "dbus-init" : "dbus-iface-added");
+    }
+}
+
+static gboolean
+_dbus_handle_interface_removed(NMBluezManager *   self,
+                               const char *       object_path,
+                               BzDBusObj **       inout_bzobj,
+                               const char *const *removed_interfaces)
+{
+    gboolean   changed = FALSE;
+    BzDBusObj *bzobj;
+    gsize      i;
+
+    if (inout_bzobj && *inout_bzobj) {
+        bzobj = *inout_bzobj;
+        nm_assert(bzobj == _bzobjs_get(self, object_path));
+    } else {
+        bzobj = _bzobjs_get(self, object_path);
+        if (!bzobj)
+            return FALSE;
+        NM_SET_OUT(inout_bzobj, bzobj);
+    }
+
+    for (i = 0; removed_interfaces[i]; i++) {
+        const char *interface_name = removed_interfaces[i];
+
+        if (nm_streq(interface_name, NM_BLUEZ5_ADAPTER_INTERFACE)) {
+            if (bzobj->d_has_adapter_iface) {
+                changed                    = TRUE;
+                bzobj->d_has_adapter_iface = FALSE;
+            }
+            if (bzobj->d_adapter.address) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_adapter.address);
+            }
+            if (bzobj->d_adapter_powered) {
+                changed                  = TRUE;
+                bzobj->d_adapter_powered = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_DEVICE_INTERFACE)) {
+            if (bzobj->d_has_device_iface) {
+                changed                   = TRUE;
+                bzobj->d_has_device_iface = FALSE;
+            }
+            if (bzobj->d_device.address) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.address);
+            }
+            if (bzobj->d_device.name) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.name);
+            }
+            if (bzobj->d_device.adapter) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_device.adapter);
+            }
+            if (bzobj->d_device_capabilities != NM_BT_CAPABILITY_NONE) {
+                changed                      = TRUE;
+                bzobj->d_device_capabilities = NM_BT_CAPABILITY_NONE;
+            }
+            if (bzobj->d_device_connected) {
+                changed                   = TRUE;
+                bzobj->d_device_connected = FALSE;
+            }
+            if (bzobj->d_device_paired) {
+                changed                = TRUE;
+                bzobj->d_device_paired = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_INTERFACE)) {
+            if (bzobj->d_has_network_iface) {
+                changed                    = TRUE;
+                bzobj->d_has_network_iface = FALSE;
+            }
+            if (bzobj->d_network.interface) {
+                changed = TRUE;
+                nm_clear_g_free(&bzobj->d_network.interface);
+            }
+            if (bzobj->d_network_connected) {
+                changed                    = TRUE;
+                bzobj->d_network_connected = FALSE;
+            }
+        } else if (nm_streq(interface_name, NM_BLUEZ5_NETWORK_SERVER_INTERFACE)) {
+            if (bzobj->d_has_network_server_iface) {
+                changed                           = TRUE;
+                bzobj->d_has_network_server_iface = FALSE;
+            }
+        }
+    }
+
+    return changed;
+}
+
+static void
+_dbus_managed_objects_changed_cb(GDBusConnection *connection,
+                                 const char *     sender_name,
+                                 const char *     arg_object_path,
+                                 const char *     interface_name,
+                                 const char *     signal_name,
+                                 GVariant *       parameters,
+                                 gpointer         user_data)
+{
+    NMBluezManager *       self  = user_data;
+    NMBluezManagerPrivate *priv  = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    BzDBusObj *            bzobj = NULL;
+    gboolean               changed;
+
+    nm_assert(nm_streq0(interface_name, DBUS_INTERFACE_OBJECT_MANAGER));
+
+    if (priv->get_managed_objects_cancellable) {
+        /* we still wait for the initial GetManagedObjects(). Ignore the event. */
+        return;
+    }
+
+    if (nm_streq(signal_name, "InterfacesAdded")) {
+        gs_unref_variant GVariant *interfaces_and_properties = NULL;
+        const char *               object_path;
+
+        if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oa{sa{sv}})")))
+            return;
+
+        g_variant_get(parameters, "(&o@a{sa{sv}})", &object_path, &interfaces_and_properties);
+
+        _dbus_handle_interface_added(self, object_path, interfaces_and_properties, FALSE);
+        return;
+    }
+
+    if (nm_streq(signal_name, "InterfacesRemoved")) {
+        gs_free const char **interfaces = NULL;
+        const char *         object_path;
+
+        if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(oas)")))
+            return;
+
+        g_variant_get(parameters, "(&o^a&s)", &object_path, &interfaces);
+
+        changed = _dbus_handle_interface_removed(self, object_path, &bzobj, interfaces);
+        if (changed)
+            _dbus_process_changes(self, bzobj, "dbus-iface-removed");
+        return;
+    }
+}
+
+static void
+_dbus_properties_changed_cb(GDBusConnection *connection,
+                            const char *     sender_name,
+                            const char *     object_path,
+                            const char *     signal_interface_name,
+                            const char *     signal_name,
+                            GVariant *       parameters,
+                            gpointer         user_data)
+{
+    NMBluezManager *       self = user_data;
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    const char *           interface_name;
+    gs_unref_variant GVariant *changed_properties     = NULL;
+    gs_free const char **      invalidated_properties = NULL;
+    BzDBusObj *                bzobj                  = NULL;
+
+    if (priv->get_managed_objects_cancellable) {
+        /* we still wait for the initial GetManagedObjects(). Ignore the event. */
+        return;
+    }
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sa{sv}as)")))
+        return;
+
+    g_variant_get(parameters,
+                  "(&s@a{sv}^a&s)",
+                  &interface_name,
+                  &changed_properties,
+                  &invalidated_properties);
+
+    if (_dbus_handle_properties_changed(self,
+                                        object_path,
+                                        interface_name,
+                                        changed_properties,
+                                        invalidated_properties,
+                                        &bzobj))
+        _dbus_process_changes(self, bzobj, "dbus-property-changed");
+}
+
+static void
+_dbus_get_managed_objects_cb(GVariant *result, GError *error, gpointer user_data)
+{
+    NMBluezManager *       self;
+    NMBluezManagerPrivate *priv;
+    GVariantIter           iter;
+    const char *           object_path;
+    GVariant *             ifaces;
+
+    if (!result && nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->get_managed_objects_cancellable);
+
+    if (!result) {
+        _LOGT("initial GetManagedObjects() call failed: %s", error->message);
+        _cleanup_for_name_owner(self);
+        return;
+    }
+
+    _LOGT("initial GetManagedObjects call succeeded");
+
+    g_variant_iter_init(&iter, result);
+    while (g_variant_iter_next(&iter, "{&o@a{sa{sv}}}", &object_path, &ifaces)) {
+        _nm_unused gs_unref_variant GVariant *ifaces_free = ifaces;
+
+        _dbus_handle_interface_added(self, object_path, ifaces, TRUE);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_for_name_owner(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv                             = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    gboolean               emit_device_availability_changed = FALSE;
+    GHashTableIter         iter;
+    BzDBusObj *            bzobj;
+    gboolean               first = TRUE;
+
+    nm_clear_g_cancellable(&priv->get_managed_objects_cancellable);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->managed_objects_changed_id);
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->properties_changed_id);
+
+    nm_clear_g_free(&priv->name_owner);
+
+    g_hash_table_iter_init(&iter, priv->bzobjs);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &bzobj, NULL)) {
+        if (first) {
+            first = FALSE;
+            _LOGT("drop all objects form D-Bus cache...");
+        }
+        _dbus_handle_interface_removed(self,
+                                       bzobj->object_path,
+                                       &bzobj,
+                                       ALL_RELEVANT_INTERFACE_NAMES);
+        nm_c_list_move_tail(&priv->process_change_lst_head, &bzobj->process_change_lst);
+    }
+    _process_change_idle_all(self, &emit_device_availability_changed);
+    nm_assert(g_hash_table_size(priv->bzobjs) == 0);
+
+    if (emit_device_availability_changed)
+        nm_manager_notify_device_availability_maybe_changed(priv->manager);
+}
+
+static void
+name_owner_changed(NMBluezManager *self, const char *owner)
+{
+    _nm_unused gs_unref_object NMBluezManager *self_keep_alive = g_object_ref(self);
+    NMBluezManagerPrivate *                    priv            = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    owner = nm_str_not_empty(owner);
+
+    if (!owner)
+        _LOGT("D-Bus name for bluez has no owner");
+    else
+        _LOGT("D-Bus name for bluez has owner %s", owner);
+
+    nm_clear_g_cancellable(&priv->name_owner_get_cancellable);
+
+    if (nm_streq0(priv->name_owner, owner))
+        return;
+
+    _cleanup_for_name_owner(self);
+
+    if (!owner)
+        return;
+
+    priv->name_owner = g_strdup(owner);
+
+    priv->get_managed_objects_cancellable = g_cancellable_new();
+
+    priv->managed_objects_changed_id =
+        nm_dbus_connection_signal_subscribe_object_manager(priv->dbus_connection,
+                                                           priv->name_owner,
+                                                           NM_BLUEZ_MANAGER_PATH,
+                                                           NULL,
+                                                           _dbus_managed_objects_changed_cb,
+                                                           self,
+                                                           NULL);
+
+    priv->properties_changed_id =
+        nm_dbus_connection_signal_subscribe_properties_changed(priv->dbus_connection,
+                                                               priv->name_owner,
+                                                               NULL,
+                                                               NULL,
+                                                               _dbus_properties_changed_cb,
+                                                               self,
+                                                               NULL);
+
+    nm_dbus_connection_call_get_managed_objects(priv->dbus_connection,
+                                                priv->name_owner,
+                                                NM_BLUEZ_MANAGER_PATH,
+                                                G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                                20000,
+                                                priv->get_managed_objects_cancellable,
+                                                _dbus_get_managed_objects_cb,
+                                                self);
+}
+
+static void
+name_owner_changed_cb(GDBusConnection *connection,
+                      const char *     sender_name,
+                      const char *     object_path,
+                      const char *     interface_name,
+                      const char *     signal_name,
+                      GVariant *       parameters,
+                      gpointer         user_data)
+{
+    NMBluezManager *self = user_data;
+    const char *    new_owner;
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
+        return;
+
+    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
+
+    name_owner_changed(self, new_owner);
+}
+
+static void
+name_owner_get_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+    if (name_owner || !g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        name_owner_changed(user_data, name_owner);
+}
+
+/*****************************************************************************/
+
+static void
+_cleanup_all(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    priv->settings_registered = FALSE;
+
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_added, self);
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_updated, self);
+    g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_removed, self);
+
+    g_hash_table_remove_all(priv->conn_data_elems);
+    g_hash_table_remove_all(priv->conn_data_heads);
+
+    _cleanup_for_name_owner(self);
+
+    nm_clear_g_cancellable(&priv->name_owner_get_cancellable);
+
+    nm_clear_g_dbus_connection_signal(priv->dbus_connection, &priv->name_owner_changed_id);
+}
+
+static void
+start(NMDeviceFactory *factory)
+{
+    NMBluezManager *             self;
+    NMBluezManagerPrivate *      priv;
+    NMSettingsConnection *const *sett_conns;
+    guint                        n_sett_conns;
+    guint                        i;
+
+    g_return_if_fail(NM_IS_BLUEZ_MANAGER(factory));
+
+    self = NM_BLUEZ_MANAGER(factory);
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    _cleanup_all(self);
+
+    if (!priv->dbus_connection) {
+        _LOGI("no D-Bus connection available");
+        return;
+    }
+
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_ADDED,
+                     G_CALLBACK(cp_connection_added),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
+                     G_CALLBACK(cp_connection_updated),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
+                     G_CALLBACK(cp_connection_removed),
+                     self);
+
+    priv->settings_registered = TRUE;
+
+    sett_conns = nm_settings_get_connections(priv->settings, &n_sett_conns);
+    for (i = 0; i < n_sett_conns; i++)
+        _conn_track_update(self, sett_conns[i], TRUE, NULL, NULL, NULL);
+
+    priv->name_owner_changed_id =
+        nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
+                                                               NM_BLUEZ_SERVICE,
+                                                               name_owner_changed_cb,
+                                                               self,
+                                                               NULL);
+
+    priv->name_owner_get_cancellable = g_cancellable_new();
+
+    nm_dbus_connection_call_get_name_owner(priv->dbus_connection,
+                                           NM_BLUEZ_SERVICE,
+                                           10000,
+                                           priv->name_owner_get_cancellable,
+                                           name_owner_get_cb,
+                                           self);
+}
+
+/*****************************************************************************/
+
+static void
+_connect_returned(NMBluezManager *        self,
+                  BzDBusObj *             bzobj,
+                  NMBluetoothCapabilities bt_type,
+                  const char *            device_name,
+                  NMBluez5DunContext *    dun_context,
+                  GError *                error)
+{
+    char sbuf_cap[100];
+
+    if (error) {
+        nm_assert(!device_name);
+        nm_assert(!dun_context);
+
+        _LOGI("%s [%s]: connect failed: %s",
+              nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                                sbuf_cap,
+                                                sizeof(sbuf_cap)),
+              bzobj->object_path,
+              error->message);
+
+        _device_connect_req_data_complete(g_steal_pointer(&bzobj->x_device.c_req_data),
+                                          self,
+                                          NULL,
+                                          error);
+        _connect_disconnect(self, bzobj, "cleanup after connect failure");
+        return;
+    }
+
+    nm_assert(bzobj->x_device_connect_bt_type == bt_type);
+    nm_assert(device_name);
+    nm_assert((bt_type == NM_BT_CAPABILITY_DUN) == (!!dun_context));
+    nm_assert(bzobj->x_device.c_req_data);
+
+    g_clear_object(&bzobj->x_device.c_req_data->int_cancellable);
+
+    bzobj->x_device.connect_dun_context = dun_context;
+
+    _LOGD("%s [%s]: connect successful to device %s",
+          nm_bluetooth_capability_to_string(bzobj->x_device_connect_bt_type,
+                                            sbuf_cap,
+                                            sizeof(sbuf_cap)),
+          bzobj->object_path,
+          device_name);
+
+    /* we already have another over-all timer running. But after we connected the device,
+     * we still need to wait for bluez to acknowledge the connected state (via D-Bus, for NAP).
+     * For DUN profiles we likely are already fully connected by now.
+     *
+     * Anyway, schedule another timeout that is possibly shorter than the overall, original
+     * timeout. Now this should go down fast. */
+    bzobj->x_device.c_req_data->timeout_wait_connect_id =
+        g_timeout_add(5000, _connect_timeout_wait_connected_cb, bzobj),
+    bzobj->x_device.c_req_data->device_name = g_strdup(device_name);
+
+    if (_bzobjs_device_is_usable(bzobj, NULL, NULL) && _bzobjs_device_is_connected(bzobj)) {
+        /* We are now connected. Schedule the task that completes the state. */
+        _process_change_idle_schedule(self, bzobj);
+    }
+}
+
+#if WITH_BLUEZ5_DUN
+static void
+_connect_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    _connect_disconnect(bzobj->self, bzobj, "DUN connection hung up");
+}
+
+static void
+_connect_dun_step2_cb(NMBluez5DunContext *context,
+                      const char *        rfcomm_dev,
+                      GError *            error,
+                      gpointer            user_data)
+{
+    BzDBusObj *bzobj;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (rfcomm_dev) {
+        /* We want to early notify about the rfcomm path. That is because we might still delay
+         * to signal full activation longer (asynchronously). But the earliest time the callback
+         * is invoked with the rfcomm path, we just created the device synchronously.
+         *
+         * By already notifying the caller about the path early, it avoids a race where ModemManager
+         * would find the modem before the bluetooth code considers the profile fully activated. */
+
+        nm_assert(!error);
+        nm_assert(bzobj->x_device.c_req_data);
+
+        if (!g_cancellable_is_cancelled(bzobj->x_device.c_req_data->ext_cancellable))
+            bzobj->x_device.c_req_data->callback(bzobj->self,
+                                                 FALSE,
+                                                 rfcomm_dev,
+                                                 NULL,
+                                                 bzobj->x_device.c_req_data->callback_user_data);
+
+        if (!context) {
+            /* No context set. This means, we just got notified about the rfcomm path and need to wait
+             * longer, for the next callback. */
+            return;
+        }
+    }
+
+    _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, rfcomm_dev, context, error);
+}
+
+static void
+_connect_dun_step1_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret = NULL;
+    gs_free_error GError *error    = NULL;
+    BzDBusObj *           bzobj_adapter;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    bzobj = user_data;
+
+    if (error) {
+        _LOGT("DUN: [%s]: bluetooth device connect failed: %s", bzobj->object_path, error->message);
+        /* we actually ignore this error. Let's try, maybe we still can connect via DUN. */
+        g_clear_error(&error);
+    } else
+        _LOGT("DUN: [%s]: bluetooth device connected successfully", bzobj->object_path);
+
+    if (!_bzobjs_device_is_usable(bzobj, &bzobj_adapter, NULL)) {
+        nm_utils_error_set(&error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "device %s is not usable for DUN after connect",
+                           bzobj->object_path);
+        _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+        return;
+    }
+
+    if (!nm_bluez5_dun_connect(bzobj_adapter->d_adapter.address,
+                               bzobj->d_device.address,
+                               bzobj->x_device.c_req_data->int_cancellable,
+                               _connect_dun_step2_cb,
+                               bzobj,
+                               _connect_dun_notify_tty_hangup_cb,
+                               bzobj,
+                               &error)) {
+        _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_DUN, NULL, NULL, error);
+        return;
+    }
+}
+#endif
+
+static void
+_connect_nap_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *ret                = NULL;
+    const char *               network_iface_name = NULL;
+    gs_free_error GError *error                   = NULL;
+    BzDBusObj *           bzobj;
+
+    ret = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error);
+
+    if (!ret && nm_utils_error_is_cancelled(error))
+        return;
+
+    if (ret)
+        g_variant_get(ret, "(&s)", &network_iface_name);
+
+    bzobj = user_data;
+
+    _connect_returned(bzobj->self, bzobj, NM_BT_CAPABILITY_NAP, network_iface_name, NULL, error);
+}
+
+static void
+_connect_cancelled_cb(GCancellable *cancellable, BzDBusObj *bzobj)
+{
+    _connect_disconnect(bzobj->self, bzobj, "connect cancelled");
+}
+
+static gboolean
+_connect_timeout_wait_connected_cb(gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    bzobj->x_device.c_req_data->timeout_wait_connect_id = 0;
+    _connect_disconnect(bzobj->self, bzobj, "timeout waiting for connected");
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_timeout_cb(gpointer user_data)
+{
+    BzDBusObj *bzobj = user_data;
+
+    bzobj->x_device.c_req_data->timeout_id = 0;
+    _connect_disconnect(bzobj->self, bzobj, "timeout connecting");
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_connect_disconnect(NMBluezManager *self, BzDBusObj *bzobj, const char *reason)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+    DeviceConnectReqData * c_req_data;
+    char                   sbuf_cap[100];
+    gboolean               bt_type;
+
+    if (bzobj->x_device_connect_bt_type == NM_BT_CAPABILITY_NONE) {
+        nm_assert(!bzobj->x_device.c_req_data);
+        return;
+    }
+
+    bt_type = bzobj->x_device_connect_bt_type;
+    nm_assert(NM_IN_SET(bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP));
+    bzobj->x_device_connect_bt_type = NM_BT_CAPABILITY_NONE;
+
+    c_req_data = g_steal_pointer(&bzobj->x_device.c_req_data);
+
+    _LOGD("%s [%s]: disconnect due to %s",
+          nm_bluetooth_capability_to_string(bt_type, sbuf_cap, sizeof(sbuf_cap)),
+          bzobj->object_path,
+          reason);
+
+    if (c_req_data)
+        nm_clear_g_cancellable(&c_req_data->int_cancellable);
+
+    if (bt_type == NM_BT_CAPABILITY_DUN) {
+        /* For DUN devices, we also called org.bluez.Device1.Connect() (because in order
+         * for nm_bluez5_dun_connect() to succeed, we need to be already connected *why??).
+         *
+         * But upon disconnect we don't call Disconnect() because we don't know whether somebody
+         * else also uses the bluetooth device for other purposes. During disconnect we only
+         * terminate the DUN connection, but don't disconnect entirely. I think that's the
+         * best we can do. */
+#if WITH_BLUEZ5_DUN
+        nm_clear_pointer(&bzobj->x_device.connect_dun_context, nm_bluez5_dun_disconnect);
+#else
+        nm_assert_not_reached();
+#endif
+    } else {
+        if (priv->name_owner) {
+            gs_unref_object GCancellable *cancellable = NULL;
+
+            cancellable = g_cancellable_new();
+
+            nm_shutdown_wait_obj_register_cancellable_full(
+                cancellable,
+                g_strdup_printf("bt-disconnect-nap[%s]", bzobj->object_path),
+                TRUE);
+
+            g_dbus_connection_call(priv->dbus_connection,
+                                   priv->name_owner,
+                                   bzobj->object_path,
+                                   NM_BLUEZ5_NETWORK_INTERFACE,
+                                   "Disconnect",
+                                   g_variant_new("()"),
+                                   NULL,
+                                   G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                   -1,
+                                   cancellable,
+                                   _dbus_call_complete_cb_nop,
+                                   NULL);
+        }
+    }
+
+    if (c_req_data) {
+        gs_free_error GError *error = NULL;
+
+        nm_utils_error_set(&error, NM_UTILS_ERROR_UNKNOWN, "connect aborted due to %s", reason);
+        _device_connect_req_data_complete(c_req_data, self, NULL, error);
+    }
+}
+
+gboolean
+nm_bluez_manager_connect(NMBluezManager *        self,
+                         const char *            object_path,
+                         NMBluetoothCapabilities connection_bt_type,
+                         int                     timeout_msec,
+                         GCancellable *          cancellable,
+                         NMBluezManagerConnectCb callback,
+                         gpointer                callback_user_data,
+                         GError **               error)
+{
+    gs_unref_object GCancellable *int_cancellable = NULL;
+    DeviceConnectReqData *        c_req_data;
+    NMBluezManagerPrivate *       priv;
+    BzDBusObj *                   bzobj;
+    char                          sbuf_cap[100];
+
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), FALSE);
+    g_return_val_if_fail(NM_IN_SET(connection_bt_type, NM_BT_CAPABILITY_DUN, NM_BT_CAPABILITY_NAP),
+                         FALSE);
+    g_return_val_if_fail(callback, FALSE);
+
+    nm_assert(timeout_msec > 0);
+
+    priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    bzobj = _bzobjs_get(self, object_path);
+
+    if (!bzobj) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s does not exist", object_path);
+        return FALSE;
+    }
+
+    if (!_bzobjs_device_is_usable(bzobj, NULL, NULL)) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "device %s is not usable", object_path);
+        return FALSE;
+    }
+
+    if (!NM_FLAGS_ALL(bzobj->d_device_capabilities, connection_bt_type)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "device %s has not the required capabilities",
+                           object_path);
+        return FALSE;
+    }
+
+#if !WITH_BLUEZ5_DUN
+    if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "DUN is not supported");
+        return FALSE;
+    }
+#endif
+
+    _connect_disconnect(self, bzobj, "new activation");
+
+    _LOGD("%s [%s]: connecting...",
+          nm_bluetooth_capability_to_string(connection_bt_type, sbuf_cap, sizeof(sbuf_cap)),
+          bzobj->object_path);
+
+    int_cancellable = g_cancellable_new();
+
+#if WITH_BLUEZ5_DUN
+    if (connection_bt_type == NM_BT_CAPABILITY_DUN) {
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_DEVICE_INTERFACE,
+                               "Connect",
+                               NULL,
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               timeout_msec,
+                               int_cancellable,
+                               _connect_dun_step1_cb,
+                               bzobj);
+    } else
+#endif
+    {
+        nm_assert(connection_bt_type == NM_BT_CAPABILITY_NAP);
+        g_dbus_connection_call(priv->dbus_connection,
+                               priv->name_owner,
+                               bzobj->object_path,
+                               NM_BLUEZ5_NETWORK_INTERFACE,
+                               "Connect",
+                               g_variant_new("(s)", BLUETOOTH_CONNECT_NAP),
+                               G_VARIANT_TYPE("(s)"),
+                               G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                               timeout_msec,
+                               int_cancellable,
+                               _connect_nap_cb,
+                               bzobj);
+    }
+
+    c_req_data  = g_slice_new(DeviceConnectReqData);
+    *c_req_data = (DeviceConnectReqData){
+        .int_cancellable    = g_steal_pointer(&int_cancellable),
+        .ext_cancellable    = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+        .ext_cancelled_id =
+            g_signal_connect(cancellable, "cancelled", G_CALLBACK(_connect_cancelled_cb), bzobj),
+        .timeout_id = g_timeout_add(timeout_msec, _connect_timeout_cb, bzobj),
+    };
+
+    bzobj->x_device_connect_bt_type = connection_bt_type;
+    bzobj->x_device.c_req_data      = c_req_data;
+
+    return TRUE;
+}
+
+void
+nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path)
+{
+    BzDBusObj *bzobj;
+
+    g_return_if_fail(NM_IS_BLUEZ_MANAGER(self));
+    g_return_if_fail(object_path);
+
+    bzobj = _bzobjs_get(self, object_path);
+    if (!bzobj)
+        return;
+
+    _connect_disconnect(self, bzobj, "disconnected by user");
+}
+
+/*****************************************************************************/
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    *out_ignore = TRUE;
+    g_return_val_if_fail(plink->type == NM_LINK_TYPE_BNEP, NULL);
+    return NULL;
+}
+
+static gboolean
+match_connection(NMDeviceFactory *factory, NMConnection *connection)
+{
+    const char *type = nm_connection_get_connection_type(connection);
+
+    nm_assert(nm_streq(type, NM_SETTING_BLUETOOTH_SETTING_NAME));
+
+    if (_nm_connection_get_setting_bluetooth_for_nap(connection))
+        return FALSE; /* handled by the bridge factory */
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_bluez_manager_init(NMBluezManager *self)
+{
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    priv->vtable_network_server = (NMBtVTableNetworkServer){
+        .is_available      = _network_server_vt_is_available,
+        .register_bridge   = _network_server_vt_register_bridge,
+        .unregister_bridge = _network_server_vt_unregister_bridge,
+    };
+
+    c_list_init(&priv->network_server_lst_head);
+    c_list_init(&priv->process_change_lst_head);
+
+    priv->conn_data_heads =
+        g_hash_table_new_full(_conn_data_head_hash, _conn_data_head_equal, g_free, NULL);
+    priv->conn_data_elems = g_hash_table_new_full(nm_pdirect_hash,
+                                                  nm_pdirect_equal,
+                                                  nm_g_slice_free_fcn(ConnDataElem),
+                                                  NULL);
+
+    priv->bzobjs = g_hash_table_new_full(nm_pstr_hash,
+                                         nm_pstr_equal,
+                                         (GDestroyNotify) _bz_dbus_obj_free,
+                                         NULL);
+
+    priv->manager         = g_object_ref(NM_MANAGER_GET);
+    priv->settings        = g_object_ref(NM_SETTINGS_GET);
+    priv->dbus_connection = nm_g_object_ref(NM_MAIN_DBUS_CONNECTION_GET);
+
+    g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server,
+                                          NULL,
+                                          &priv->vtable_network_server);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMBluezManager *       self = NM_BLUEZ_MANAGER(object);
+    NMBluezManagerPrivate *priv = NM_BLUEZ_MANAGER_GET_PRIVATE(self);
+
+    /* FIXME(shutdown): we need a nm_device_factory_stop() hook to first unregister all
+     *   BzDBusObj instances and do necessary cleanup actions (like disconnecting devices
+     *   or deleting panu_connection). */
+
+    nm_assert(c_list_is_empty(&priv->network_server_lst_head));
+    nm_assert(c_list_is_empty(&priv->process_change_lst_head));
+    nm_assert(priv->process_change_idle_id == 0);
+
+    g_atomic_pointer_compare_and_exchange(&nm_bt_vtable_network_server,
+                                          &priv->vtable_network_server,
+                                          NULL);
+
+    _cleanup_all(self);
+
+    G_OBJECT_CLASS(nm_bluez_manager_parent_class)->dispose(object);
+
+    g_clear_object(&priv->settings);
+    g_clear_object(&priv->manager);
+    g_clear_object(&priv->dbus_connection);
+
+    nm_clear_pointer(&priv->bzobjs, g_hash_table_destroy);
+}
+
+static void
+nm_bluez_manager_class_init(NMBluezManagerClass *klass)
+{
+    GObjectClass *        object_class  = G_OBJECT_CLASS(klass);
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->create_device       = create_device;
+    factory_class->match_connection    = match_connection;
+    factory_class->start               = start;
+}
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.h b/src/core/devices/bluetooth/nm-bluez-manager.h
new file mode 100644
index 00000000..04bfea7d
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez-manager.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ */
+
+#ifndef __NM_BLUEZ_MANAGER_H__
+#define __NM_BLUEZ_MANAGER_H__
+
+#define NM_TYPE_BLUEZ_MANAGER (nm_bluez_manager_get_type())
+#define NM_BLUEZ_MANAGER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManager))
+#define NM_BLUEZ_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
+#define NM_IS_BLUEZ_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_BLUEZ_MANAGER))
+#define NM_IS_BLUEZ_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_BLUEZ_MANAGER))
+#define NM_BLUEZ_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_BLUEZ_MANAGER, NMBluezManagerClass))
+
+typedef struct _NMBluezManager      NMBluezManager;
+typedef struct _NMBluezManagerClass NMBluezManagerClass;
+
+GType nm_bluez_manager_get_type(void);
+
+typedef void (*NMBluezManagerConnectCb)(
+    NMBluezManager *self,
+    gboolean        is_completed /* or else is early notification with DUN path */,
+    const char *    device_name,
+    GError *        error,
+    gpointer        user_data);
+
+gboolean nm_bluez_manager_connect(NMBluezManager *        self,
+                                  const char *            object_path,
+                                  NMBluetoothCapabilities connection_bt_type,
+                                  int                     timeout_msec,
+                                  GCancellable *          cancellable,
+                                  NMBluezManagerConnectCb callback,
+                                  gpointer                callback_user_data,
+                                  GError **               error);
+
+void nm_bluez_manager_disconnect(NMBluezManager *self, const char *object_path);
+
+#endif /* __NM_BLUEZ_MANAGER_H__ */
diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.c b/src/core/devices/bluetooth/nm-bluez5-dun.c
new file mode 100644
index 00000000..e29884d8
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez5-dun.c
@@ -0,0 +1,857 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <sys/socket.h>
+#include <bluetooth/sdp.h>
+#include <bluetooth/sdp_lib.h>
+#include <bluetooth/rfcomm.h>
+#include <net/ethernet.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "nm-bluez5-dun.h"
+#include "nm-bt-error.h"
+#include "NetworkManagerUtils.h"
+
+#define RFCOMM_FMT "/dev/rfcomm%d"
+
+/*****************************************************************************/
+
+typedef struct {
+    GCancellable *       cancellable;
+    NMBluez5DunConnectCb callback;
+    gpointer             callback_user_data;
+
+    sdp_session_t *sdp_session;
+
+    GError *rfcomm_sdp_search_error;
+
+    GSource *source;
+
+    gint64 connect_open_tty_started_at;
+
+    gulong cancelled_id;
+
+    guint8 sdp_session_try_count;
+} ConnectData;
+
+struct _NMBluez5DunContext {
+    const char *dst_str;
+
+    ConnectData *cdat;
+
+    NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb;
+    gpointer                     notify_tty_hangup_user_data;
+
+    char *rfcomm_tty_path;
+
+    GSource *rfcomm_tty_poll_source;
+
+    int rfcomm_sock_fd;
+    int rfcomm_tty_fd;
+    int rfcomm_tty_no;
+    int rfcomm_channel;
+
+    bdaddr_t src;
+    bdaddr_t dst;
+
+    char src_str[];
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_BT
+#define _NMLOG_PREFIX_NAME "bluez"
+#define _NMLOG(level, context, ...)                                       \
+    G_STMT_START                                                          \
+    {                                                                     \
+        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {               \
+            const NMBluez5DunContext *const _context = (context);         \
+                                                                          \
+            _nm_log((level),                                              \
+                    (_NMLOG_DOMAIN),                                      \
+                    0,                                                    \
+                    NULL,                                                 \
+                    NULL,                                                 \
+                    "%s: DUN[%s] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),    \
+                    _NMLOG_PREFIX_NAME,                                   \
+                    _context->src_str _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                 \
+    }                                                                     \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void     _context_invoke_callback_success(NMBluez5DunContext *context);
+static void     _context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error);
+static void     _context_free(NMBluez5DunContext *context);
+static int      _connect_open_tty(NMBluez5DunContext *context);
+static gboolean _connect_sdp_session_start(NMBluez5DunContext *context, GError **error);
+
+/*****************************************************************************/
+
+NM_AUTO_DEFINE_FCN0(NMBluez5DunContext *, _nm_auto_free_context, _context_free);
+#define nm_auto_free_context nm_auto(_nm_auto_free_context)
+
+/*****************************************************************************/
+
+const char *
+nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context)
+{
+    return context->src_str;
+}
+
+const char *
+nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context)
+{
+    return context->dst_str;
+}
+
+const char *
+nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context)
+{
+    return context->rfcomm_tty_path;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_rfcomm_tty_poll_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+
+    _LOGD(context,
+          "receive %s%s%s signal on rfcomm file descriptor",
+          NM_FLAGS_HAS(condition, G_IO_ERR) ? "ERR" : "",
+          NM_FLAGS_ALL(condition, G_IO_HUP | G_IO_ERR) ? "," : "",
+          NM_FLAGS_HAS(condition, G_IO_HUP) ? "HUP" : "");
+
+    nm_clear_g_source_inst(&context->rfcomm_tty_poll_source);
+    context->notify_tty_hangup_cb(context, context->notify_tty_hangup_user_data);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_open_tty_retry_cb(gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    int                 r;
+
+    r = _connect_open_tty(context);
+    if (r >= 0)
+        return G_SOURCE_REMOVE;
+
+    if (nm_utils_get_monotonic_timestamp_nsec()
+        > context->cdat->connect_open_tty_started_at + (30 * 100 * NM_UTILS_NSEC_PER_MSEC)) {
+        gs_free_error GError *error = NULL;
+
+        nm_clear_g_source_inst(&context->cdat->source);
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "give up waiting to open %s device: %s (%d)",
+                    context->rfcomm_tty_path,
+                    nm_strerror_native(r),
+                    -r);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+static int
+_connect_open_tty(NMBluez5DunContext *context)
+{
+    int fd;
+    int errsv;
+
+    fd = open(context->rfcomm_tty_path, O_RDONLY | O_NOCTTY | O_CLOEXEC);
+    if (fd < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+
+        if (!context->cdat->source) {
+            _LOGD(context,
+                  "failed opening tty " RFCOMM_FMT ": %s (%d). Start polling...",
+                  context->rfcomm_tty_no,
+                  nm_strerror_native(errsv),
+                  errsv);
+            context->cdat->connect_open_tty_started_at = nm_utils_get_monotonic_timestamp_nsec();
+            context->cdat->source                      = nm_g_timeout_source_new(100,
+                                                            G_PRIORITY_DEFAULT,
+                                                            _connect_open_tty_retry_cb,
+                                                            context,
+                                                            NULL);
+            g_source_attach(context->cdat->source, NULL);
+        }
+        return -errsv;
+    }
+
+    context->rfcomm_tty_fd = fd;
+
+    context->rfcomm_tty_poll_source = nm_g_unix_fd_source_new(context->rfcomm_tty_fd,
+                                                              G_IO_ERR | G_IO_HUP,
+                                                              G_PRIORITY_DEFAULT,
+                                                              _rfcomm_tty_poll_cb,
+                                                              context,
+                                                              NULL);
+    g_source_attach(context->rfcomm_tty_poll_source, NULL);
+
+    _context_invoke_callback_success(context);
+    return 0;
+}
+
+static void
+_connect_create_rfcomm(NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+    struct rfcomm_dev_req req;
+    int                   devid;
+    int                   errsv;
+    int                   r;
+
+    _LOGD(context, "connected to %s on channel %d", context->dst_str, context->rfcomm_channel);
+
+    /* Create an RFCOMM kernel device for the DUN channel */
+    memset(&req, 0, sizeof(req));
+    req.dev_id  = -1;
+    req.flags   = (1 << RFCOMM_REUSE_DLC) | (1 << RFCOMM_RELEASE_ONHUP);
+    req.channel = context->rfcomm_channel;
+    memcpy(&req.src, &context->src, ETH_ALEN);
+    memcpy(&req.dst, &context->dst, ETH_ALEN);
+    devid = ioctl(context->rfcomm_sock_fd, RFCOMMCREATEDEV, &req);
+    if (devid < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        if (errsv == EBADFD) {
+            /* hm. We use a non-blocking socket to connect. Above getsockopt(SOL_SOCKET,SO_ERROR) indicated
+             * success, but still now we fail with EBADFD. I think that is a bug and we should get the
+             * failure during connect().
+             *
+             * Anyway, craft a less confusing error message than
+             * "failed to create rfcomm device: File descriptor in bad state (77)". */
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "unknown failure to connect to DUN device");
+        } else {
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "failed to create rfcomm device: %s (%d)",
+                        nm_strerror_native(errsv),
+                        errsv);
+        }
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    context->rfcomm_tty_no   = devid;
+    context->rfcomm_tty_path = g_strdup_printf(RFCOMM_FMT, devid);
+
+    r = _connect_open_tty(context);
+    if (r < 0) {
+        /* we created the rfcomm device, but cannot yet open it. That means, we are
+         * not yet fully connected. However, we notify the caller about "what we learned
+         * so far". Note that this happens synchronously.
+         *
+         * The purpose is that once we proceed synchronously, modem-manager races with
+         * the detection of the modem. We want to notify the caller first about the
+         * device name. */
+        context->cdat->callback(NULL,
+                                context->rfcomm_tty_path,
+                                NULL,
+                                context->cdat->callback_user_data);
+    }
+}
+
+static gboolean
+_connect_socket_connect_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+    int                   errsv = 0;
+    socklen_t             slen  = sizeof(errsv);
+    int                   r;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    r = getsockopt(context->rfcomm_sock_fd, SOL_SOCKET, SO_ERROR, &errsv, &slen);
+
+    if (r < 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to complete connecting RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (errsv != 0) {
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to connect RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    _connect_create_rfcomm(context);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_connect_socket_connect(NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+    struct sockaddr_rc    sa;
+    int                   errsv;
+
+    context->rfcomm_sock_fd =
+        socket(AF_BLUETOOTH, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, BTPROTO_RFCOMM);
+    if (context->rfcomm_sock_fd < 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to create RFCOMM socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    /* Connect to the remote device */
+    memset(&sa, 0, sizeof(sa));
+    sa.rc_family  = AF_BLUETOOTH;
+    sa.rc_channel = 0;
+    memcpy(&sa.rc_bdaddr, &context->src, ETH_ALEN);
+    if (bind(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) {
+        errsv = errno;
+        g_set_error(&error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to bind socket: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return;
+    }
+
+    memset(&sa, 0, sizeof(sa));
+    sa.rc_family  = AF_BLUETOOTH;
+    sa.rc_channel = context->rfcomm_channel;
+    memcpy(&sa.rc_bdaddr, &context->dst, ETH_ALEN);
+    if (connect(context->rfcomm_sock_fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) {
+        errsv = errno;
+        if (errsv != EINPROGRESS) {
+            g_set_error(&error,
+                        NM_BT_ERROR,
+                        NM_BT_ERROR_DUN_CONNECT_FAILED,
+                        "failed to connect to remote device: %s (%d)",
+                        nm_strerror_native(errsv),
+                        errsv);
+            _context_invoke_callback_fail_and_free(context, error);
+            return;
+        }
+
+        _LOGD(context,
+              "connecting to %s on channel %d...",
+              context->dst_str,
+              context->rfcomm_channel);
+
+        context->cdat->source = nm_g_unix_fd_source_new(context->rfcomm_sock_fd,
+                                                        G_IO_OUT,
+                                                        G_PRIORITY_DEFAULT,
+                                                        _connect_socket_connect_cb,
+                                                        context,
+                                                        NULL);
+        g_source_attach(context->cdat->source, NULL);
+        return;
+    }
+
+    _connect_create_rfcomm(context);
+}
+
+static void
+_connect_sdp_search_cb(uint8_t type, uint16_t status, uint8_t *rsp, size_t size, void *user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    int                 scanned;
+    int                 seqlen    = 0;
+    int                 bytesleft = size;
+    uint8_t             dataType;
+    int                 channel = -1;
+
+    if (context->cdat->rfcomm_sdp_search_error || context->rfcomm_channel >= 0)
+        return;
+
+    _LOGD(context, "SDP search finished with type=%d status=%d", status, type);
+
+    /* SDP response received */
+    if (status || type != SDP_SVC_SEARCH_ATTR_RSP) {
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "did not get a Service Discovery response");
+        return;
+    }
+
+    scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
+
+    _LOGD(context, "SDP sequence type scanned=%d length=%d", scanned, seqlen);
+
+    scanned = sdp_extract_seqtype(rsp, bytesleft, &dataType, &seqlen);
+    if (!scanned || !seqlen) {
+        /* Short read or unknown sequence type */
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "improper Service Discovery response");
+        return;
+    }
+
+    rsp += scanned;
+    bytesleft -= scanned;
+    do {
+        sdp_record_t *rec;
+        int           recsize = 0;
+        sdp_list_t *  protos;
+
+        rec = sdp_extract_pdu(rsp, bytesleft, &recsize);
+        if (!rec)
+            break;
+
+        if (!recsize) {
+            sdp_record_free(rec);
+            break;
+        }
+
+        if (sdp_get_access_protos(rec, &protos) == 0) {
+            /* Extract the DUN channel number */
+            channel = sdp_get_proto_port(protos, RFCOMM_UUID);
+            sdp_list_free(protos, NULL);
+
+            _LOGD(context, "SDP channel=%d", channel);
+        }
+        sdp_record_free(rec);
+
+        scanned += recsize;
+        rsp += recsize;
+        bytesleft -= recsize;
+    } while (scanned < (ssize_t) size && bytesleft > 0 && channel < 0);
+
+    if (channel == -1) {
+        g_set_error(&context->cdat->rfcomm_sdp_search_error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "did not receive rfcomm-channel");
+        return;
+    }
+
+    context->rfcomm_channel = channel;
+}
+
+static gboolean
+_connect_sdp_search_io_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+    int                   errsv;
+
+    if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL)) {
+        _LOGD(context, "SDP search returned with invalid IO condition 0x%x", (guint) condition);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "Service Discovery interrupted");
+        nm_clear_g_source_inst(&context->cdat->source);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (sdp_process(context->cdat->sdp_session) == 0) {
+        _LOGD(context, "SDP search still not finished");
+        return G_SOURCE_CONTINUE;
+    }
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    if (context->rfcomm_channel < 0 && !context->cdat->rfcomm_sdp_search_error) {
+        errsv = sdp_get_error(context->cdat->sdp_session);
+        _LOGD(context, "SDP search failed: %s (%d)", nm_strerror_native(errsv), errsv);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "Service Discovery failed with %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        _context_invoke_callback_fail_and_free(context, error);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (context->cdat->rfcomm_sdp_search_error) {
+        _LOGD(context,
+              "SDP search failed to complete: %s",
+              context->cdat->rfcomm_sdp_search_error->message);
+        _context_invoke_callback_fail_and_free(context, context->cdat->rfcomm_sdp_search_error);
+        return G_SOURCE_REMOVE;
+    }
+
+    nm_clear_pointer(&context->cdat->sdp_session, sdp_close);
+
+    _connect_socket_connect(context);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_sdp_session_start_on_idle_cb(gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    gs_free_error GError *error = NULL;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    _LOGD(context, "retry starting sdp-session...");
+
+    if (!_connect_sdp_session_start(context, &error))
+        _context_invoke_callback_fail_and_free(context, error);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_connect_sdp_io_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    NMBluez5DunContext *context = user_data;
+    sdp_list_t *        search;
+    sdp_list_t *        attrs;
+    uuid_t              svclass;
+    uint16_t            attr;
+    int                 errsv;
+    int                 fd_err = 0;
+    int                 r;
+    socklen_t           len     = sizeof(fd_err);
+    gs_free_error GError *error = NULL;
+
+    nm_clear_g_source_inst(&context->cdat->source);
+
+    _LOGD(context, "sdp-session ready to connect with fd=%d", fd);
+
+    if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &fd_err, &len) < 0) {
+        errsv = NM_ERRNO_NATIVE(errno);
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error for getsockopt on Service Discovery socket: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    if (fd_err != 0) {
+        errsv = nm_errno_native(fd_err);
+
+        if (NM_IN_SET(errsv, ECONNREFUSED, EHOSTDOWN)
+            && --context->cdat->sdp_session_try_count > 0) {
+            /* *sigh* */
+            _LOGD(context,
+                  "sdp-session failed with %s (%d). Retry in a bit",
+                  nm_strerror_native(errsv),
+                  errsv);
+            nm_clear_g_source_inst(&context->cdat->source);
+            context->cdat->source = nm_g_timeout_source_new(1000,
+                                                            G_PRIORITY_DEFAULT,
+                                                            _connect_sdp_session_start_on_idle_cb,
+                                                            context,
+                                                            NULL);
+            g_source_attach(context->cdat->source, NULL);
+            return G_SOURCE_REMOVE;
+        }
+
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error on Service Discovery socket: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    if (sdp_set_notify(context->cdat->sdp_session, _connect_sdp_search_cb, context) < 0) {
+        /* Should not be reached, only can fail if we passed bad sdp_session. */
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "could not set Service Discovery notification");
+        goto done;
+    }
+
+    sdp_uuid16_create(&svclass, DIALUP_NET_SVCLASS_ID);
+    search = sdp_list_append(NULL, &svclass);
+    attr   = SDP_ATTR_PROTO_DESC_LIST;
+    attrs  = sdp_list_append(NULL, &attr);
+
+    r = sdp_service_search_attr_async(context->cdat->sdp_session,
+                                      search,
+                                      SDP_ATTR_REQ_INDIVIDUAL,
+                                      attrs);
+
+    sdp_list_free(attrs, NULL);
+    sdp_list_free(search, NULL);
+
+    if (r < 0) {
+        errsv = nm_errno_native(sdp_get_error(context->cdat->sdp_session));
+        error = g_error_new(NM_BT_ERROR,
+                            NM_BT_ERROR_DUN_CONNECT_FAILED,
+                            "error starting Service Discovery: %s (%d)",
+                            nm_strerror_native(errsv),
+                            errsv);
+        goto done;
+    }
+
+    /* Set callback responsible for update the internal SDP transaction */
+    context->cdat->source = nm_g_unix_fd_source_new(fd,
+                                                    G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _connect_sdp_search_io_cb,
+                                                    context,
+                                                    NULL);
+    g_source_attach(context->cdat->source, NULL);
+
+done:
+    if (error)
+        _context_invoke_callback_fail_and_free(context, error);
+    return G_SOURCE_REMOVE;
+}
+
+/*****************************************************************************/
+
+static void
+_connect_cancelled_cb(GCancellable *cancellable, NMBluez5DunContext *context)
+{
+    gs_free_error GError *error = NULL;
+
+    if (!g_cancellable_set_error_if_cancelled(cancellable, &error))
+        g_return_if_reached();
+
+    _context_invoke_callback_fail_and_free(context, error);
+}
+
+static gboolean
+_connect_sdp_session_start(NMBluez5DunContext *context, GError **error)
+{
+    nm_assert(context->cdat);
+
+    nm_clear_g_source_inst(&context->cdat->source);
+    nm_clear_pointer(&context->cdat->sdp_session, sdp_close);
+
+    context->cdat->sdp_session = sdp_connect(&context->src, &context->dst, SDP_NON_BLOCKING);
+    if (!context->cdat->sdp_session) {
+        int errsv = nm_errno_native(errno);
+
+        g_set_error(error,
+                    NM_BT_ERROR,
+                    NM_BT_ERROR_DUN_CONNECT_FAILED,
+                    "failed to connect to the SDP server: %s (%d)",
+                    nm_strerror_native(errsv),
+                    errsv);
+        return FALSE;
+    }
+
+    context->cdat->source = nm_g_unix_fd_source_new(sdp_get_socket(context->cdat->sdp_session),
+                                                    G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
+                                                    G_PRIORITY_DEFAULT,
+                                                    _connect_sdp_io_cb,
+                                                    context,
+                                                    NULL);
+    g_source_attach(context->cdat->source, NULL);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_bluez5_dun_connect(const char *                 adapter,
+                      const char *                 remote,
+                      GCancellable *               cancellable,
+                      NMBluez5DunConnectCb         callback,
+                      gpointer                     callback_user_data,
+                      NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb,
+                      gpointer                     notify_tty_hangup_user_data,
+                      GError **                    error)
+{
+    nm_auto_free_context NMBluez5DunContext *context = NULL;
+    ConnectData *                            cdat;
+    gsize                                    src_l;
+    gsize                                    dst_l;
+
+    g_return_val_if_fail(adapter, FALSE);
+    g_return_val_if_fail(remote, FALSE);
+    g_return_val_if_fail(G_IS_CANCELLABLE(cancellable), FALSE);
+    g_return_val_if_fail(callback, FALSE);
+    g_return_val_if_fail(notify_tty_hangup_cb, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+    nm_assert(!g_cancellable_is_cancelled(cancellable));
+
+    src_l = strlen(adapter) + 1;
+    dst_l = strlen(remote) + 1;
+
+    cdat  = g_slice_new(ConnectData);
+    *cdat = (ConnectData){
+        .callback              = callback,
+        .callback_user_data    = callback_user_data,
+        .cancellable           = g_object_ref(cancellable),
+        .sdp_session_try_count = 5,
+    };
+
+    context  = g_malloc(sizeof(NMBluez5DunContext) + src_l + dst_l);
+    *context = (NMBluez5DunContext){
+        .cdat                        = cdat,
+        .notify_tty_hangup_cb        = notify_tty_hangup_cb,
+        .notify_tty_hangup_user_data = notify_tty_hangup_user_data,
+        .rfcomm_tty_no               = -1,
+        .rfcomm_sock_fd              = -1,
+        .rfcomm_tty_fd               = -1,
+        .rfcomm_channel              = -1,
+    };
+    memcpy(&context->src_str[0], adapter, src_l);
+    context->dst_str = &context->src_str[src_l];
+    memcpy((char *) context->dst_str, remote, dst_l);
+
+    if (str2ba(adapter, &context->src) < 0) {
+        g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid source");
+        return FALSE;
+    }
+
+    if (str2ba(remote, &context->dst) < 0) {
+        g_set_error(error, NM_BT_ERROR, NM_BT_ERROR_DUN_CONNECT_FAILED, "invalid remote");
+        return FALSE;
+    }
+
+    context->cdat->cancelled_id = g_signal_connect(context->cdat->cancellable,
+                                                   "cancelled",
+                                                   G_CALLBACK(_connect_cancelled_cb),
+                                                   context);
+
+    if (!_connect_sdp_session_start(context, error))
+        return FALSE;
+
+    _LOGD(context, "starting channel number discovery for device %s", context->dst_str);
+
+    g_steal_pointer(&context);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+void
+nm_bluez5_dun_disconnect(NMBluez5DunContext *context)
+{
+    nm_assert(context);
+    nm_assert(!context->cdat);
+
+    _LOGD(context, "disconnecting DUN connection");
+
+    _context_free(context);
+}
+
+/*****************************************************************************/
+
+static void
+_context_cleanup_connect_data(NMBluez5DunContext *context)
+{
+    ConnectData *cdat;
+
+    cdat = g_steal_pointer(&context->cdat);
+    if (!cdat)
+        return;
+
+    nm_clear_g_signal_handler(cdat->cancellable, &cdat->cancelled_id);
+
+    nm_clear_g_source_inst(&cdat->source);
+
+    nm_clear_pointer(&cdat->sdp_session, sdp_close);
+
+    g_clear_object(&cdat->cancellable);
+
+    g_clear_error(&cdat->rfcomm_sdp_search_error);
+
+    nm_g_slice_free(cdat);
+}
+
+static void
+_context_invoke_callback(NMBluez5DunContext *context, GError *error)
+{
+    NMBluez5DunConnectCb callback;
+    gpointer             callback_user_data;
+
+    nm_assert(context);
+    nm_assert(context->cdat);
+    nm_assert(context->cdat->callback);
+    nm_assert(error || context->rfcomm_tty_path);
+
+    if (!error)
+        _LOGD(context, "connected via \"%s\"", context->rfcomm_tty_path);
+    else if (nm_utils_error_is_cancelled(error))
+        _LOGD(context, "cancelled");
+    else
+        _LOGD(context, "failed to connect: %s", error->message);
+
+    callback           = context->cdat->callback;
+    callback_user_data = context->cdat->callback_user_data;
+
+    _context_cleanup_connect_data(context);
+
+    callback(error ? NULL : context,
+             error ? NULL : context->rfcomm_tty_path,
+             error,
+             callback_user_data);
+}
+
+static void
+_context_invoke_callback_success(NMBluez5DunContext *context)
+{
+    nm_assert(context->rfcomm_tty_path);
+    _context_invoke_callback(context, NULL);
+}
+
+static void
+_context_invoke_callback_fail_and_free(NMBluez5DunContext *context, GError *error)
+{
+    nm_assert(error);
+    _context_invoke_callback(context, error);
+    _context_free(context);
+}
+
+static void
+_context_free(NMBluez5DunContext *context)
+{
+    nm_assert(context);
+
+    _context_cleanup_connect_data(context);
+
+    nm_clear_g_source_inst(&context->rfcomm_tty_poll_source);
+
+    if (context->rfcomm_sock_fd >= 0) {
+        if (context->rfcomm_tty_no >= 0) {
+            struct rfcomm_dev_req req;
+
+            memset(&req, 0, sizeof(struct rfcomm_dev_req));
+            req.dev_id             = context->rfcomm_tty_no;
+            context->rfcomm_tty_no = -1;
+            (void) ioctl(context->rfcomm_sock_fd, RFCOMMRELEASEDEV, &req);
+        }
+        nm_close(nm_steal_fd(&context->rfcomm_sock_fd));
+    }
+
+    if (context->rfcomm_tty_fd >= 0)
+        nm_close(nm_steal_fd(&context->rfcomm_tty_fd));
+    nm_clear_g_free(&context->rfcomm_tty_path);
+    g_free(context);
+}
diff --git a/src/core/devices/bluetooth/nm-bluez5-dun.h b/src/core/devices/bluetooth/nm-bluez5-dun.h
new file mode 100644
index 00000000..020d4119
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bluez5-dun.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NM_BLUEZ5_DUN_H__
+#define __NM_BLUEZ5_DUN_H__
+
+typedef struct _NMBluez5DunContext NMBluez5DunContext;
+
+#if WITH_BLUEZ5_DUN
+
+typedef void (*NMBluez5DunConnectCb)(NMBluez5DunContext *context,
+                                     const char *        rfcomm_dev,
+                                     GError *            error,
+                                     gpointer            user_data);
+
+typedef void (*NMBluez5DunNotifyTtyHangupCb)(NMBluez5DunContext *context, gpointer user_data);
+
+gboolean nm_bluez5_dun_connect(const char *                 adapter,
+                               const char *                 remote,
+                               GCancellable *               cancellable,
+                               NMBluez5DunConnectCb         callback,
+                               gpointer                     callback_user_data,
+                               NMBluez5DunNotifyTtyHangupCb notify_tty_hangup_cb,
+                               gpointer                     notify_tty_hangup_user_data,
+                               GError **                    error);
+
+void nm_bluez5_dun_disconnect(NMBluez5DunContext *context);
+
+const char *nm_bluez5_dun_context_get_adapter(const NMBluez5DunContext *context);
+const char *nm_bluez5_dun_context_get_remote(const NMBluez5DunContext *context);
+const char *nm_bluez5_dun_context_get_rfcomm_dev(const NMBluez5DunContext *context);
+
+#endif /* WITH_BLUEZ5_DUN */
+
+#endif /* __NM_BLUEZ5_DUN_H__ */
diff --git a/src/core/devices/bluetooth/nm-bt-error.c b/src/core/devices/bluetooth/nm-bt-error.c
new file mode 100644
index 00000000..2e49eb7a
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bt-error.c
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-bt-error.h"
+
+NM_CACHED_QUARK_FCN("nm-bt-error", nm_bt_error_quark);
diff --git a/src/core/devices/bluetooth/nm-bt-error.h b/src/core/devices/bluetooth/nm-bt-error.h
new file mode 100644
index 00000000..432ae5b4
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-bt-error.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef _NM_BLUEZ5_ERROR_H_
+#define _NM_BLUEZ5_ERROR_H_
+
+typedef enum {
+    NM_BT_ERROR_CONNECTION_NOT_BT = 0,   /*< nick=ConnectionNotBt >*/
+    NM_BT_ERROR_CONNECTION_INVALID,      /*< nick=ConnectionInvalid >*/
+    NM_BT_ERROR_CONNECTION_INCOMPATIBLE, /*< nick=ConnectionIncompatible >*/
+    NM_BT_ERROR_DUN_CONNECT_FAILED,      /*< nick=DunConnectFailed >*/
+} NMBtError;
+
+#define NM_BT_ERROR (nm_bt_error_quark())
+GQuark nm_bt_error_quark(void);
+
+#endif /* _NM_BT_ERROR_H_ */
diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c
new file mode 100644
index 00000000..c07be2d3
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-device-bt.c
@@ -0,0 +1,1412 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2011 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-bt.h"
+
+#include <stdio.h>
+#include <linux/if_ether.h>
+
+#include "nm-core-internal.h"
+#include "nm-bluez-common.h"
+#include "nm-bluez-manager.h"
+#include "devices/nm-device-private.h"
+#include "ppp/nm-ppp-manager.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-bluetooth.h"
+#include "nm-setting-cdma.h"
+#include "nm-setting-gsm.h"
+#include "nm-setting-serial.h"
+#include "nm-setting-ppp.h"
+#include "NetworkManagerUtils.h"
+#include "settings/nm-settings-connection.h"
+#include "nm-utils.h"
+#include "nm-bt-error.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+
+#include "devices/wwan/nm-modem-manager.h"
+#include "devices/wwan/nm-modem.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceBt
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceBt,
+                             PROP_BT_BDADDR,
+                             PROP_BT_BZ_MGR,
+                             PROP_BT_CAPABILITIES,
+                             PROP_BT_DBUS_PATH,
+                             PROP_BT_NAME, );
+
+enum {
+    PPP_STATS,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    NMModemManager *modem_manager;
+
+    NMBluezManager *bz_mgr;
+
+    char *dbus_path;
+
+    char *bdaddr;
+    char *name;
+
+    char *connect_rfcomm_iface;
+
+    GSList *connect_modem_candidates;
+
+    NMModem *modem;
+
+    GCancellable *connect_bz_cancellable;
+
+    gulong connect_watch_link_id;
+
+    guint connect_watch_link_idle_id;
+
+    guint connect_wait_modem_id;
+
+    NMBluetoothCapabilities capabilities : 6;
+
+    NMBluetoothCapabilities connect_bt_type : 6; /* BT type of the current connection */
+
+    NMDeviceStageState stage1_bt_state : 3;
+    NMDeviceStageState stage1_modem_prepare_state : 3;
+
+    bool is_connected : 1;
+
+    bool mm_running : 1;
+
+} NMDeviceBtPrivate;
+
+struct _NMDeviceBt {
+    NMDevice          parent;
+    NMDeviceBtPrivate _priv;
+};
+
+struct _NMDeviceBtClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceBt, nm_device_bt, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_BT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMDeviceBt, NM_IS_DEVICE_BT, NMDevice)
+
+/*****************************************************************************/
+
+NMBluetoothCapabilities
+nm_device_bt_get_capabilities(NMDeviceBt *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_BT(self), NM_BT_CAPABILITY_NONE);
+
+    return NM_DEVICE_BT_GET_PRIVATE(self)->capabilities;
+}
+
+static NMBluetoothCapabilities
+get_connection_bt_type(NMConnection *connection)
+{
+    NMSettingBluetooth *s_bt;
+    const char *        bt_type;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+
+    if (s_bt) {
+        bt_type = nm_setting_bluetooth_get_connection_type(s_bt);
+        if (bt_type) {
+            if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_DUN))
+                return NM_BT_CAPABILITY_DUN;
+            else if (nm_streq(bt_type, NM_SETTING_BLUETOOTH_TYPE_PANU))
+                return NM_BT_CAPABILITY_NAP;
+        }
+    }
+
+    return NM_BT_CAPABILITY_NONE;
+}
+
+static gboolean
+get_connection_bt_type_check(NMDeviceBt *             self,
+                             NMConnection *           connection,
+                             NMBluetoothCapabilities *out_bt_type,
+                             GError **                error)
+{
+    NMBluetoothCapabilities bt_type;
+
+    bt_type = get_connection_bt_type(connection);
+
+    NM_SET_OUT(out_bt_type, bt_type);
+
+    if (bt_type == NM_BT_CAPABILITY_NONE) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "profile is not a PANU/DUN bluetooth type");
+        return FALSE;
+    }
+
+    if (!NM_FLAGS_ALL(NM_DEVICE_BT_GET_PRIVATE(self)->capabilities, bt_type)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device does not support bluetooth type");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_NON_KERNEL;
+}
+
+static gboolean
+can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    NMDeviceBt *            self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *     priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMBluetoothCapabilities bt_type;
+
+    nm_assert(!specific_object || !*specific_object);
+
+    if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)->can_auto_connect(device, sett_conn, NULL))
+        return FALSE;
+
+    if (!get_connection_bt_type_check(self,
+                                      nm_settings_connection_get_connection(sett_conn),
+                                      &bt_type,
+                                      NULL))
+        return FALSE;
+
+    /* Can't auto-activate a DUN connection without ModemManager */
+    if (bt_type == NM_BT_CAPABILITY_DUN && priv->mm_running == FALSE)
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceBt *        self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMSettingBluetooth *s_bt;
+    const char *        bdaddr;
+
+    if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (!get_connection_bt_type_check(self, connection, NULL, error))
+        return FALSE;
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+
+    bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (!bdaddr) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "profile lacks bdaddr setting");
+        return FALSE;
+    }
+    if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, bdaddr, -1)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "devices bdaddr setting mismatches");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceBt *            self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *     priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMBluetoothCapabilities bt_type;
+
+    if (!get_connection_bt_type_check(self, connection, &bt_type, error))
+        return FALSE;
+
+    if (bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "ModemManager missing for DUN profile");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceBtPrivate * priv = NM_DEVICE_BT_GET_PRIVATE(device);
+    NMSettingBluetooth *s_bt;
+    const char *        setting_bdaddr;
+    const char *        ctype;
+    gboolean            is_dun = FALSE;
+    gboolean            is_pan = FALSE;
+    NMSettingGsm *      s_gsm;
+    NMSettingCdma *     s_cdma;
+    NMSettingSerial *   s_serial;
+    NMSettingPpp *      s_ppp;
+    const char *        fallback_prefix = NULL, *preferred = NULL;
+
+    s_gsm    = nm_connection_get_setting_gsm(connection);
+    s_cdma   = nm_connection_get_setting_cdma(connection);
+    s_serial = nm_connection_get_setting_serial(connection);
+    s_ppp    = nm_connection_get_setting_ppp(connection);
+
+    s_bt = nm_connection_get_setting_bluetooth(connection);
+    if (!s_bt) {
+        s_bt = (NMSettingBluetooth *) nm_setting_bluetooth_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_bt));
+    }
+
+    ctype = nm_setting_bluetooth_get_connection_type(s_bt);
+    if (ctype) {
+        if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_DUN))
+            is_dun = TRUE;
+        else if (!strcmp(ctype, NM_SETTING_BLUETOOTH_TYPE_PANU))
+            is_pan = TRUE;
+    } else {
+        if (s_gsm || s_cdma)
+            is_dun = TRUE;
+        else if (priv->capabilities & NM_BT_CAPABILITY_NAP)
+            is_pan = TRUE;
+    }
+
+    if (is_pan) {
+        /* Make sure the device supports PAN */
+        if (!(priv->capabilities & NM_BT_CAPABILITY_NAP)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("PAN requested, but Bluetooth device does not support NAP"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_TYPE);
+            return FALSE;
+        }
+
+        /* PAN can't use any DUN-related settings */
+        if (s_gsm || s_cdma || s_serial || s_ppp) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("PAN connections cannot specify GSM, CDMA, or serial settings"));
+            g_prefix_error(error,
+                           "%s: ",
+                           s_gsm      ? NM_SETTING_GSM_SETTING_NAME
+                           : s_cdma   ? NM_SETTING_CDMA_SETTING_NAME
+                           : s_serial ? NM_SETTING_SERIAL_SETTING_NAME
+                                      : NM_SETTING_PPP_SETTING_NAME);
+            return FALSE;
+        }
+
+        g_object_set(G_OBJECT(s_bt),
+                     NM_SETTING_BLUETOOTH_TYPE,
+                     NM_SETTING_BLUETOOTH_TYPE_PANU,
+                     NULL);
+
+        fallback_prefix = _("PAN connection");
+    } else if (is_dun) {
+        /* Make sure the device supports PAN */
+        if (!(priv->capabilities & NM_BT_CAPABILITY_DUN)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("DUN requested, but Bluetooth device does not support DUN"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_TYPE);
+            return FALSE;
+        }
+
+        /* Need at least a GSM or a CDMA setting */
+        if (!s_gsm && !s_cdma) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("DUN connection must include a GSM or CDMA setting"));
+            g_prefix_error(error, "%s: ", NM_SETTING_BLUETOOTH_SETTING_NAME);
+            return FALSE;
+        }
+
+        g_object_set(G_OBJECT(s_bt),
+                     NM_SETTING_BLUETOOTH_TYPE,
+                     NM_SETTING_BLUETOOTH_TYPE_DUN,
+                     NULL);
+
+        if (s_gsm) {
+            fallback_prefix = _("GSM connection");
+        } else {
+            fallback_prefix = _("CDMA connection");
+            if (!nm_setting_cdma_get_number(s_cdma))
+                g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL);
+        }
+    } else {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Unknown/unhandled Bluetooth connection type"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_BLUETOOTH_SETTING_NAME,
+                       NM_SETTING_BLUETOOTH_TYPE);
+        return FALSE;
+    }
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_BLUETOOTH_SETTING_NAME,
+                              existing_connections,
+                              preferred,
+                              fallback_prefix,
+                              NULL,
+                              NULL,
+                              is_dun ? FALSE : TRUE); /* No IPv6 yet for DUN */
+
+    setting_bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+    if (setting_bdaddr) {
+        /* Make sure the setting BT Address (if any) matches the device's */
+        if (!nm_utils_hwaddr_matches(setting_bdaddr, -1, priv->bdaddr, -1)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("connection does not match device"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_BLUETOOTH_SETTING_NAME,
+                           NM_SETTING_BLUETOOTH_BDADDR);
+            return FALSE;
+        }
+    } else {
+        /* Lock the connection to this device by default */
+        if (!nm_utils_hwaddr_matches(priv->bdaddr, -1, NULL, ETH_ALEN))
+            g_object_set(G_OBJECT(s_bt), NM_SETTING_BLUETOOTH_BDADDR, priv->bdaddr, NULL);
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* IP method PPP */
+
+static void
+ppp_stats(NMModem *modem, guint i_in_bytes, guint i_out_bytes, gpointer user_data)
+{
+    guint32 in_bytes  = i_in_bytes;
+    guint32 out_bytes = i_out_bytes;
+
+    g_signal_emit(NM_DEVICE_BT(user_data),
+                  signals[PPP_STATS],
+                  0,
+                  (guint) in_bytes,
+                  (guint) out_bytes);
+}
+
+static void
+ppp_failed(NMModem *modem, guint i_reason, gpointer user_data)
+{
+    NMDevice *          device = NM_DEVICE(user_data);
+    NMDeviceBt *        self   = NM_DEVICE_BT(user_data);
+    NMDeviceStateReason reason = i_reason;
+
+    switch (nm_device_get_state(device)) {
+    case NM_DEVICE_STATE_PREPARE:
+    case NM_DEVICE_STATE_CONFIG:
+    case NM_DEVICE_STATE_NEED_AUTH:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        break;
+    case NM_DEVICE_STATE_IP_CONFIG:
+    case NM_DEVICE_STATE_IP_CHECK:
+    case NM_DEVICE_STATE_SECONDARIES:
+    case NM_DEVICE_STATE_ACTIVATED:
+        if (nm_device_activate_ip4_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET);
+        else if (nm_device_activate_ip6_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET6);
+        else if (nm_device_activate_ip4_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else if (nm_device_activate_ip6_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else {
+            _LOGW(LOGD_MB,
+                  "PPP failure in unexpected state %u",
+                  (guint) nm_device_get_state(device));
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+modem_auth_requested(NMModem *modem, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled
+     * during activation.
+     */
+    if (!nm_device_is_activating(device))
+        return;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
+}
+
+static void
+modem_auth_result(NMModem *modem, GError *error, gpointer user_data)
+{
+    NMDevice *         device = NM_DEVICE(user_data);
+    NMDeviceBtPrivate *priv   = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+
+    if (error) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data)
+{
+    NMDeviceBt *        self   = user_data;
+    NMDeviceBtPrivate * priv   = NM_DEVICE_BT_GET_PRIVATE(self);
+    NMDeviceStateReason reason = i_reason;
+    NMDeviceState       state;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+
+    g_return_if_fail(NM_IN_SET(state, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_NEED_AUTH));
+
+    nm_assert(priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING);
+
+    if (!success) {
+        if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT) {
+            /* If the connect failed because the SIM PIN was wrong don't allow
+             * the device to be auto-activated anymore, which would risk locking
+             * the SIM if the incorrect PIN continues to be used.
+             */
+            nm_device_autoconnect_blocked_set(NM_DEVICE(self),
+                                              NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN);
+        }
+
+        nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason);
+        return;
+    }
+
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    if (priv->modem)
+        nm_modem_device_state_changed(priv->modem, new_state, old_state);
+
+    /* Need to recheck available connections whenever MM appears or disappears,
+     * since the device could be both DUN and NAP capable and thus may not
+     * change state (which rechecks available connections) when MM comes and goes.
+     */
+    if (priv->mm_running && NM_FLAGS_HAS(priv->capabilities, NM_BT_CAPABILITY_DUN))
+        nm_device_recheck_available_connections(device);
+}
+
+static void
+modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data)
+{
+    NMDeviceBt *self   = NM_DEVICE_BT(user_data);
+    NMDevice *  device = NM_DEVICE(self);
+
+    g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP4 | LOGD_BT,
+              "retrieving IP4 configuration failed: %s",
+              error->message);
+        nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config));
+}
+
+static void
+ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_is_activating(device))
+        return;
+
+    if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+modem_cleanup(NMDeviceBt *self)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->modem) {
+        g_signal_handlers_disconnect_matched(priv->modem,
+                                             G_SIGNAL_MATCH_DATA,
+                                             0,
+                                             0,
+                                             NULL,
+                                             NULL,
+                                             self);
+        nm_clear_pointer(&priv->modem, nm_modem_unclaim);
+    }
+}
+
+static void
+modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data)
+{
+    NMModemState  new_state = new_state_i;
+    NMModemState  old_state = old_state_i;
+    NMDevice *    device    = NM_DEVICE(user_data);
+    NMDeviceState dev_state = nm_device_get_state(device);
+
+    if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING) {
+        /* Will be called whenever something external to NM disables the
+         * modem directly through ModemManager.
+         */
+        if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DISCONNECTED,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+    }
+
+    if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING
+        && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) {
+        /* Fail the device if the modem disconnects unexpectedly while the
+         * device is activating/activated. */
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+}
+
+static void
+modem_removed_cb(NMModem *modem, gpointer user_data)
+{
+    NMDeviceBt *  self = NM_DEVICE_BT(user_data);
+    NMDeviceState state;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (nm_device_is_activating(NM_DEVICE(self)) || state == NM_DEVICE_STATE_ACTIVATED) {
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    modem_cleanup(self);
+}
+
+static gboolean
+modem_try_claim(NMDeviceBt *self, NMModem *modem)
+{
+    NMDeviceBtPrivate *priv             = NM_DEVICE_BT_GET_PRIVATE(self);
+    gs_free char *     rfcomm_base_name = NULL;
+    NMDeviceState      state;
+
+    if (priv->modem) {
+        if (priv->modem == modem)
+            return TRUE;
+        return FALSE;
+    }
+
+    if (nm_modem_is_claimed(modem))
+        return FALSE;
+
+    if (!priv->connect_rfcomm_iface)
+        return FALSE;
+
+    rfcomm_base_name = g_path_get_basename(priv->connect_rfcomm_iface);
+    if (!nm_streq0(rfcomm_base_name, nm_modem_get_control_port(modem)))
+        return FALSE;
+
+    /* Can only accept the modem in stage1, but since the interface matched
+     * what we were expecting, don't let anything else claim the modem either.
+     */
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (state != NM_DEVICE_STATE_PREPARE) {
+        _LOGD(LOGD_BT | LOGD_MB,
+              "modem found but device not in correct state (%d)",
+              nm_device_get_state(NM_DEVICE(self)));
+        return FALSE;
+    }
+
+    priv->modem                      = nm_modem_claim(modem);
+    priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    g_signal_connect(modem, NM_MODEM_PPP_STATS, G_CALLBACK(ppp_stats), self);
+    g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self);
+    g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self);
+    g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self);
+    g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self);
+    g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self);
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_IP_IFINDEX,
+                     G_CALLBACK(ip_ifindex_changed_cb),
+                     self);
+
+    _LOGD(LOGD_BT | LOGD_MB, "modem found");
+
+    return TRUE;
+}
+
+static void
+mm_modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data)
+{
+    NMDeviceBt *       self = user_data;
+    NMDeviceBtPrivate *priv;
+
+    if (!modem_try_claim(user_data, modem))
+        return;
+
+    priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED)
+        nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+/*****************************************************************************/
+
+void
+_nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    connected = !!connected;
+    if (priv->is_connected == connected)
+        return;
+
+    priv->is_connected = connected;
+
+    if (connected || priv->stage1_bt_state != NM_DEVICE_STAGE_STATE_COMPLETED
+        || nm_device_get_state(NM_DEVICE(self)) > NM_DEVICE_STATE_ACTIVATED) {
+        _LOGT(LOGD_BT, "set-connected: %d", connected);
+        return;
+    }
+
+    _LOGT(LOGD_BT, "set-connected: %d (disconnecting device...)", connected);
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_CARRIER);
+}
+
+static gboolean
+connect_watch_link_idle_cb(gpointer user_data)
+{
+    NMDeviceBt *       self = user_data;
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    int                ifindex;
+
+    priv->connect_watch_link_idle_id = 0;
+
+    if (nm_device_get_state(NM_DEVICE(self)) <= NM_DEVICE_STATE_ACTIVATED) {
+        ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+        if (ifindex > 0
+            && !nm_platform_link_get(nm_device_get_platform(NM_DEVICE(self)), ifindex)) {
+            _LOGT(LOGD_BT, "device disappeared");
+            nm_device_state_changed(NM_DEVICE(self),
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_BT_FAILED);
+        }
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+connect_watch_link_cb(NMPlatform *    platform,
+                      int             obj_type_i,
+                      int             ifindex,
+                      NMPlatformLink *info,
+                      int             change_type_i,
+                      NMDevice *      self)
+{
+    const NMPlatformSignalChangeType change_type = change_type_i;
+    NMDeviceBtPrivate *              priv;
+
+    /* bluez doesn't notify us when the connection disconnects.
+     * Neither does NMManager (or NMDevice) tell us when the ip-ifindex goes away.
+     * This is horrible, and should be improved. For now, watch the link ourself... */
+
+    if (NM_IN_SET(change_type, NM_PLATFORM_SIGNAL_CHANGED, NM_PLATFORM_SIGNAL_REMOVED)) {
+        priv = NM_DEVICE_BT_GET_PRIVATE(self);
+        if (priv->connect_watch_link_idle_id == 0)
+            priv->connect_watch_link_idle_id = g_idle_add(connect_watch_link_idle_cb, self);
+    }
+}
+
+static gboolean
+connect_wait_modem_timeout(gpointer user_data)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(user_data);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    /* since this timeout is longer than the connect timeout, we must have already
+     * hit the connect-timeout first or being connected. */
+    nm_assert(priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_COMPLETED);
+
+    priv->connect_wait_modem_id = 0;
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    if (priv->modem)
+        _LOGD(LOGD_BT, "timeout connecting modem for DUN connection");
+    else
+        _LOGD(LOGD_BT, "timeout finding modem for DUN connection");
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+connect_bz_cb(NMBluezManager *bz_mgr,
+              gboolean        is_complete,
+              const char *    device_name,
+              GError *        error,
+              gpointer        user_data)
+{
+    NMDeviceBt *       self;
+    NMDeviceBtPrivate *priv;
+    char               sbuf[100];
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_assert(nm_device_is_activating(NM_DEVICE(self)));
+    nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->connect_bt_type,
+                        NM_BT_CAPABILITY_DUN,
+                        NM_BT_CAPABILITY_NAP));
+
+    if (!is_complete) {
+        nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_DUN);
+        nm_assert(device_name);
+        nm_assert(!error);
+
+        if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) {
+            nm_assert(!priv->connect_rfcomm_iface);
+            _LOGD(LOGD_BT,
+                  "DUN is still connecting but got serial port \"%s\" to claim modem",
+                  device_name);
+            g_free(priv->connect_rfcomm_iface);
+            priv->connect_rfcomm_iface = g_strdup(device_name);
+        }
+        return;
+    }
+
+    g_clear_object(&priv->connect_bz_cancellable);
+
+    if (!device_name) {
+        _LOGW(LOGD_BT,
+              "%s connect request failed: %s",
+              nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)),
+              error->message);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_BT,
+          "%s connect request successful (%s)",
+          nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)),
+          device_name);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (!nm_streq0(priv->connect_rfcomm_iface, device_name)) {
+            nm_assert_not_reached();
+            g_free(priv->connect_rfcomm_iface);
+            priv->connect_rfcomm_iface = g_strdup(device_name);
+        }
+    } else {
+        nm_assert(priv->connect_bt_type == NM_BT_CAPABILITY_NAP);
+        if (!nm_device_set_ip_iface(NM_DEVICE(self), device_name)) {
+            _LOGW(LOGD_BT, "Error connecting with bluez: cannot find device %s", device_name);
+            nm_device_state_changed(NM_DEVICE(self),
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_BT_FAILED);
+            return;
+        }
+        priv->connect_watch_link_id = g_signal_connect(nm_device_get_platform(NM_DEVICE(self)),
+                                                       NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                                       G_CALLBACK(connect_watch_link_cb),
+                                                       self);
+    }
+
+    if (!priv->is_connected) {
+        /* we got the callback from NMBluezManager with success. We actually should be
+         * connected and this line shouldn't be reached. */
+        nm_assert_not_reached();
+        _LOGE(LOGD_BT, "bluetooth is unexpectedly not in connected state");
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBt *       self     = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *priv     = NM_DEVICE_BT_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
+    NMConnection *        connection;
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    priv->connect_bt_type = get_connection_bt_type(connection);
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_NONE) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_BT_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN && !priv->mm_running) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_PENDING)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    else if (priv->stage1_bt_state == NM_DEVICE_STAGE_STATE_INIT) {
+        gs_unref_object GCancellable *cancellable = NULL;
+        char                          sbuf[100];
+
+        _LOGD(LOGD_BT,
+              "connecting to %s bluetooth device",
+              nm_bluetooth_capability_to_string(priv->connect_bt_type, sbuf, sizeof(sbuf)));
+
+        cancellable = g_cancellable_new();
+
+        if (!nm_bluez_manager_connect(priv->bz_mgr,
+                                      priv->dbus_path,
+                                      priv->connect_bt_type,
+                                      30000,
+                                      cancellable,
+                                      connect_bz_cb,
+                                      self,
+                                      &error)) {
+            _LOGD(LOGD_BT, "cannot connect to bluetooth device: %s", error->message);
+            *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED;
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+
+        priv->connect_bz_cancellable = g_steal_pointer(&cancellable);
+        priv->stage1_bt_state        = NM_DEVICE_STAGE_STATE_PENDING;
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (!priv->modem) {
+            gs_free NMModem **modems = NULL;
+            guint             i, n;
+
+            if (priv->connect_wait_modem_id == 0)
+                priv->connect_wait_modem_id =
+                    g_timeout_add_seconds(30, connect_wait_modem_timeout, self);
+
+            modems = nm_modem_manager_get_modems(priv->modem_manager, &n);
+            for (i = 0; i < n; i++) {
+                if (modem_try_claim(self, modems[i]))
+                    break;
+            }
+            if (!priv->modem)
+                return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+
+        if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_PENDING)
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        if (priv->stage1_modem_prepare_state == NM_DEVICE_STAGE_STATE_INIT) {
+            priv->stage1_modem_prepare_state = NM_DEVICE_STAGE_STATE_PENDING;
+            return nm_modem_act_stage1_prepare(priv->modem,
+                                               nm_device_get_act_request(NM_DEVICE(self)),
+                                               out_failure_reason);
+        }
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(device);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN)
+        nm_modem_act_stage2_config(priv->modem);
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    nm_assert_addr_family(addr_family);
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (addr_family == AF_INET) {
+            return nm_modem_stage3_ip4_config_start(priv->modem,
+                                                    device,
+                                                    NM_DEVICE_CLASS(nm_device_bt_parent_class),
+                                                    out_failure_reason);
+        } else {
+            return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason);
+        }
+    }
+
+    return NM_DEVICE_CLASS(nm_device_bt_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(device);
+
+    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->connect_watch_link_id);
+    nm_clear_g_source(&priv->connect_watch_link_idle_id);
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_clear_g_source(&priv->connect_wait_modem_id);
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    priv->stage1_bt_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    if (priv->connect_bt_type == NM_BT_CAPABILITY_DUN) {
+        if (priv->modem) {
+            nm_modem_deactivate(priv->modem, device);
+
+            /* Since we're killing the Modem object before it'll get the
+             * state change signal, simulate the state change here.
+             */
+            nm_modem_device_state_changed(priv->modem,
+                                          NM_DEVICE_STATE_DISCONNECTED,
+                                          NM_DEVICE_STATE_ACTIVATED);
+            modem_cleanup(NM_DEVICE_BT(device));
+        }
+    }
+
+    if (priv->connect_bt_type != NM_BT_CAPABILITY_NONE) {
+        priv->connect_bt_type = NM_BT_CAPABILITY_NONE;
+        nm_bluez_manager_disconnect(priv->bz_mgr, priv->dbus_path);
+    }
+
+    nm_clear_g_free(&priv->connect_rfcomm_iface);
+
+    if (NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate)
+        NM_DEVICE_CLASS(nm_device_bt_parent_class)->deactivate(device);
+}
+
+void
+_nm_device_bt_notify_removed(NMDeviceBt *self)
+{
+    g_signal_emit_by_name(self, NM_DEVICE_REMOVED);
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_device_bt_for_same_device(NMDeviceBt *            self,
+                              const char *            dbus_path,
+                              const char *            bdaddr,
+                              const char *            name,
+                              NMBluetoothCapabilities capabilities)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    return nm_streq(priv->dbus_path, dbus_path) && nm_streq(priv->bdaddr, bdaddr)
+           && capabilities == priv->capabilities && (!name || nm_streq(priv->name, name));
+}
+
+void
+_nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_assert(name);
+
+    if (!nm_streq(priv->name, name)) {
+        _LOGT(LOGD_BT, "set-name: %s", name);
+        g_free(priv->name);
+        priv->name = g_strdup(name);
+        _notify(self, PROP_BT_NAME);
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+is_available(NMDevice *dev, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(dev);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    /* PAN doesn't need ModemManager, so devices that support it are always available */
+    if (priv->capabilities & NM_BT_CAPABILITY_NAP)
+        return TRUE;
+
+    /* DUN requires ModemManager */
+    return priv->mm_running;
+}
+
+static void
+set_mm_running(NMDeviceBt *self)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+    gboolean           running;
+
+    running = (nm_modem_manager_name_owner_get(priv->modem_manager) != NULL);
+
+    if (priv->mm_running != running) {
+        _LOGD(LOGD_BT, "ModemManager now %s", running ? "available" : "unavailable");
+
+        priv->mm_running = running;
+        nm_device_queue_recheck_available(NM_DEVICE(self),
+                                          NM_DEVICE_STATE_REASON_NONE,
+                                          NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE);
+    }
+}
+
+static void
+mm_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    set_mm_running(user_data);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_BT_NAME:
+        g_value_set_string(value, priv->name);
+        break;
+    case PROP_BT_CAPABILITIES:
+        g_value_set_uint(value, priv->capabilities);
+        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)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_BT_BZ_MGR:
+        /* construct-only */
+        priv->bz_mgr = g_object_ref(g_value_get_pointer(value));
+        nm_assert(NM_IS_BLUEZ_MANAGER(priv->bz_mgr));
+        break;
+    case PROP_BT_DBUS_PATH:
+        /* construct-only */
+        priv->dbus_path = g_value_dup_string(value);
+        nm_assert(priv->dbus_path);
+        break;
+    case PROP_BT_BDADDR:
+        /* construct-only */
+        priv->bdaddr = g_value_dup_string(value);
+        nm_assert(priv->bdaddr);
+        break;
+    case PROP_BT_NAME:
+        /* construct-only */
+        priv->name = g_value_dup_string(value);
+        nm_assert(priv->name);
+        break;
+    case PROP_BT_CAPABILITIES:
+        /* construct-only */
+        priv->capabilities = g_value_get_uint(value);
+        nm_assert(NM_IN_SET((NMBluetoothCapabilities) priv->capabilities,
+                            NM_BT_CAPABILITY_DUN,
+                            NM_BT_CAPABILITY_NAP,
+                            NM_BT_CAPABILITY_DUN | NM_BT_CAPABILITY_NAP));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_bt_init(NMDeviceBt *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(object);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->constructed(object);
+
+    priv->modem_manager = g_object_ref(nm_modem_manager_get());
+
+    nm_modem_manager_name_owner_ref(priv->modem_manager);
+
+    g_signal_connect(priv->modem_manager,
+                     NM_MODEM_MANAGER_MODEM_ADDED,
+                     G_CALLBACK(mm_modem_added_cb),
+                     self);
+
+    g_signal_connect(priv->modem_manager,
+                     "notify::" NM_MODEM_MANAGER_NAME_OWNER,
+                     G_CALLBACK(mm_name_owner_changed_cb),
+                     self);
+
+    set_mm_running(self);
+}
+
+NMDeviceBt *
+nm_device_bt_new(NMBluezManager *        bz_mgr,
+                 const char *            dbus_path,
+                 const char *            bdaddr,
+                 const char *            name,
+                 NMBluetoothCapabilities capabilities)
+{
+    g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(bz_mgr), NULL);
+    g_return_val_if_fail(dbus_path, NULL);
+    g_return_val_if_fail(bdaddr, NULL);
+    g_return_val_if_fail(name, NULL);
+    g_return_val_if_fail(capabilities != NM_BT_CAPABILITY_NONE, NULL);
+
+    return g_object_new(NM_TYPE_DEVICE_BT,
+                        NM_DEVICE_UDI,
+                        dbus_path,
+                        NM_DEVICE_IFACE,
+                        bdaddr,
+                        NM_DEVICE_DRIVER,
+                        "bluez",
+                        NM_DEVICE_PERM_HW_ADDRESS,
+                        bdaddr,
+                        NM_DEVICE_BT_BDADDR,
+                        bdaddr,
+                        NM_DEVICE_BT_BZ_MGR,
+                        bz_mgr,
+                        NM_DEVICE_BT_CAPABILITIES,
+                        (guint) capabilities,
+                        NM_DEVICE_BT_DBUS_PATH,
+                        dbus_path,
+                        NM_DEVICE_BT_NAME,
+                        name,
+                        NM_DEVICE_TYPE_DESC,
+                        "Bluetooth",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_BT,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceBt *       self = NM_DEVICE_BT(object);
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(self);
+
+    nm_clear_g_signal_handler(nm_device_get_platform(NM_DEVICE(self)),
+                              &priv->connect_watch_link_id);
+    nm_clear_g_source(&priv->connect_watch_link_idle_id);
+
+    nm_clear_g_source(&priv->connect_wait_modem_id);
+    nm_clear_g_cancellable(&priv->connect_bz_cancellable);
+
+    if (priv->modem_manager) {
+        g_signal_handlers_disconnect_by_func(priv->modem_manager,
+                                             G_CALLBACK(mm_name_owner_changed_cb),
+                                             self);
+        g_signal_handlers_disconnect_by_func(priv->modem_manager,
+                                             G_CALLBACK(mm_modem_added_cb),
+                                             self);
+        nm_modem_manager_name_owner_unref(priv->modem_manager);
+        g_clear_object(&priv->modem_manager);
+    }
+
+    modem_cleanup(self);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->dispose(object);
+
+    g_clear_object(&priv->bz_mgr);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceBtPrivate *priv = NM_DEVICE_BT_GET_PRIVATE(object);
+
+    g_free(priv->connect_rfcomm_iface);
+    g_free(priv->dbus_path);
+    g_free(priv->name);
+    g_free(priv->bdaddr);
+
+    G_OBJECT_CLASS(nm_device_bt_parent_class)->finalize(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_bluetooth = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_BLUETOOTH,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Name", "s", NM_DEVICE_BT_NAME),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("BtCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_BT_CAPABILITIES), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_bt_class_init(NMDeviceBtClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bluetooth);
+
+    device_class->connection_type_check_compatible = NM_SETTING_BLUETOOTH_SETTING_NAME;
+
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->can_auto_connect            = can_auto_connect;
+    device_class->deactivate                  = deactivate;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->act_stage3_ip_config_start  = act_stage3_ip_config_start;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->is_available                = is_available;
+    device_class->get_configured_mtu          = nm_modem_get_configured_mtu;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_BT_BZ_MGR] =
+        g_param_spec_pointer(NM_DEVICE_BT_BZ_MGR,
+                             "",
+                             "",
+                             G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_BDADDR] =
+        g_param_spec_string(NM_DEVICE_BT_BDADDR,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_DBUS_PATH] =
+        g_param_spec_string(NM_DEVICE_BT_DBUS_PATH,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_NAME] =
+        g_param_spec_string(NM_DEVICE_BT_NAME,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BT_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_BT_CAPABILITIES,
+                          "",
+                          "",
+                          NM_BT_CAPABILITY_NONE,
+                          G_MAXUINT,
+                          NM_BT_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[PPP_STATS] = g_signal_new(NM_DEVICE_BT_PPP_STATS,
+                                      G_OBJECT_CLASS_TYPE(object_class),
+                                      G_SIGNAL_RUN_FIRST,
+                                      0,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      G_TYPE_NONE,
+                                      2,
+                                      G_TYPE_UINT /*guint32 in_bytes*/,
+                                      G_TYPE_UINT /*guint32 out_bytes*/);
+}
diff --git a/src/core/devices/bluetooth/nm-device-bt.h b/src/core/devices/bluetooth/nm-device-bt.h
new file mode 100644
index 00000000..c2d3bc18
--- /dev/null
+++ b/src/core/devices/bluetooth/nm-device-bt.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_BT_H__
+#define __NETWORKMANAGER_DEVICE_BT_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_BT (nm_device_bt_get_type())
+#define NM_DEVICE_BT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BT, NMDeviceBt))
+#define NM_DEVICE_BT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BT, NMDeviceBtClass))
+#define NM_IS_DEVICE_BT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BT))
+#define NM_IS_DEVICE_BT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BT))
+#define NM_DEVICE_BT_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BT, NMDeviceBtClass))
+
+#define NM_DEVICE_BT_BDADDR       "bt-bdaddr"
+#define NM_DEVICE_BT_BZ_MGR       "bt-bz-mgr"
+#define NM_DEVICE_BT_CAPABILITIES "bt-capabilities"
+#define NM_DEVICE_BT_DBUS_PATH    "bt-dbus-path"
+#define NM_DEVICE_BT_NAME         "bt-name"
+
+#define NM_DEVICE_BT_PPP_STATS "ppp-stats"
+
+typedef struct _NMDeviceBt      NMDeviceBt;
+typedef struct _NMDeviceBtClass NMDeviceBtClass;
+
+GType nm_device_bt_get_type(void);
+
+struct _NMBluezManager;
+
+NMDeviceBt *nm_device_bt_new(struct _NMBluezManager *bz_mgr,
+                             const char *            dbus_path,
+                             const char *            bdaddr,
+                             const char *            name,
+                             NMBluetoothCapabilities capabilities);
+
+gboolean _nm_device_bt_for_same_device(NMDeviceBt *            device,
+                                       const char *            dbus_path,
+                                       const char *            bdaddr,
+                                       const char *            name,
+                                       NMBluetoothCapabilities capabilities);
+
+NMBluetoothCapabilities nm_device_bt_get_capabilities(NMDeviceBt *device);
+
+struct _NMModem;
+
+gboolean nm_device_bt_modem_added(NMDeviceBt *device, struct _NMModem *modem, const char *driver);
+
+void _nm_device_bt_notify_removed(NMDeviceBt *self);
+
+void _nm_device_bt_notify_set_name(NMDeviceBt *self, const char *name);
+
+void _nm_device_bt_notify_set_connected(NMDeviceBt *self, gboolean connected);
+
+#endif /* __NETWORKMANAGER_DEVICE_BT_H__ */
diff --git a/src/core/devices/bluetooth/tests/nm-bt-test.c b/src/core/devices/bluetooth/tests/nm-bt-test.c
new file mode 100644
index 00000000..0fc8aa87
--- /dev/null
+++ b/src/core/devices/bluetooth/tests/nm-bt-test.c
@@ -0,0 +1,223 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <glib-unix.h>
+
+#include "devices/bluetooth/nm-bluez5-dun.h"
+
+#include "nm-test-utils-core.h"
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN LOGD_BT
+#define _NMLOG(level, ...)                                             \
+    nm_log((level),                                                    \
+           _NMLOG_DOMAIN,                                              \
+           NULL,                                                       \
+           NULL,                                                       \
+           "bt%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
+           NM_PRINT_FMT_QUOTED(gl.argv_cmd, "[", gl.argv_cmd, "]", "") \
+               _NM_UTILS_MACRO_REST(__VA_ARGS__))
+
+/*****************************************************************************/
+
+struct {
+    int                argc;
+    const char *const *argv;
+    const char *       argv_cmd;
+    GMainLoop *        loop;
+} gl;
+
+typedef struct _MainCmdInfo {
+    const char *name;
+    int (*main_func)(const struct _MainCmdInfo *main_cmd_info);
+} MainCmdInfo;
+
+/*****************************************************************************/
+
+#if WITH_BLUEZ5_DUN
+
+typedef struct {
+    NMBluez5DunContext *dun_context;
+    GCancellable *      cancellable;
+    guint               timeout_id;
+    guint               sig_term_id;
+    guint               sig_int_id;
+} DunConnectData;
+
+static void
+_dun_connect_cb(NMBluez5DunContext *context,
+                const char *        rfcomm_dev,
+                GError *            error,
+                gpointer            user_data)
+{
+    DunConnectData *dun_connect_data = user_data;
+
+    g_assert(dun_connect_data);
+    g_assert(!dun_connect_data->dun_context);
+    g_assert((!!error) != (!!rfcomm_dev));
+
+    if (rfcomm_dev && !context) {
+        _LOGI("dun-connect notifies path \"%s\". Wait longer...", rfcomm_dev);
+        return;
+    }
+
+    if (rfcomm_dev) {
+        g_assert(context);
+        _LOGI("dun-connect completed with path \"%s\"", rfcomm_dev);
+    } else {
+        g_assert(!context);
+        _LOGI("dun-connect failed with error: %s", error->message);
+    }
+
+    dun_connect_data->dun_context = context;
+
+    g_main_loop_quit(gl.loop);
+}
+
+static void
+_dun_notify_tty_hangup_cb(NMBluez5DunContext *context, gpointer user_data)
+{
+    _LOGI("dun-connect: notified TTY hangup");
+}
+
+static gboolean
+_timeout_cb(gpointer user_data)
+{
+    DunConnectData *dun_connect_data = user_data;
+
+    _LOGI("timeout");
+    dun_connect_data->timeout_id = 0;
+    if (dun_connect_data->cancellable)
+        g_cancellable_cancel(dun_connect_data->cancellable);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_sig_xxx_cb(DunConnectData *dun_connect_data, int sigid)
+{
+    _LOGI("signal %s received", sigid == SIGTERM ? "SIGTERM" : "SIGINT");
+    g_main_loop_quit(gl.loop);
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_sig_term_cb(gpointer user_data)
+{
+    return _sig_xxx_cb(user_data, SIGTERM);
+}
+
+static gboolean
+_sig_int_cb(gpointer user_data)
+{
+    return _sig_xxx_cb(user_data, SIGINT);
+}
+#endif
+
+static int
+do_dun_connect(const MainCmdInfo *main_cmd_info)
+{
+#if WITH_BLUEZ5_DUN
+    gs_unref_object GCancellable *cancellable = NULL;
+    gs_free_error GError *error               = NULL;
+    const char *          adapter;
+    const char *          remote;
+    DunConnectData        dun_connect_data = {};
+
+    if (gl.argc < 4) {
+        _LOGE("missing arguments \"adapter\" and \"remote\"");
+        return -1;
+    }
+
+    adapter = gl.argv[2];
+    remote  = gl.argv[3];
+
+    cancellable                  = g_cancellable_new();
+    dun_connect_data.cancellable = cancellable;
+
+    if (!nm_bluez5_dun_connect(adapter,
+                               remote,
+                               cancellable,
+                               _dun_connect_cb,
+                               &dun_connect_data,
+                               _dun_notify_tty_hangup_cb,
+                               &dun_connect_data,
+                               &error)) {
+        _LOGE("connect failed to start: %s", error->message);
+        return -1;
+    }
+
+    dun_connect_data.timeout_id = g_timeout_add(60000, _timeout_cb, &dun_connect_data);
+
+    g_main_loop_run(gl.loop);
+
+    nm_clear_g_source(&dun_connect_data.timeout_id);
+
+    if (dun_connect_data.dun_context) {
+        dun_connect_data.sig_term_id = g_unix_signal_add(SIGTERM, _sig_term_cb, &dun_connect_data);
+        dun_connect_data.sig_int_id  = g_unix_signal_add(SIGINT, _sig_int_cb, &dun_connect_data);
+
+        g_main_loop_run(gl.loop);
+
+        nm_clear_g_source(&dun_connect_data.sig_term_id);
+        nm_clear_g_source(&dun_connect_data.sig_int_id);
+
+        nm_bluez5_dun_disconnect(g_steal_pointer(&dun_connect_data.dun_context));
+    }
+
+    return 0;
+#else
+    _LOGE("compiled without bluetooth DUN support");
+    return 1;
+#endif
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    static const MainCmdInfo main_cmd_infos[] = {
+        {
+            .name      = "dun-connect",
+            .main_func = do_dun_connect,
+        },
+    };
+    int   exit_code = 0;
+    guint i;
+
+    if (!g_getenv("G_MESSAGES_DEBUG"))
+        g_setenv("G_MESSAGES_DEBUG", "all", TRUE);
+
+    nmtst_init_with_logging(&argc, &argv, "DEBUG", "ALL");
+
+    nm_logging_init(NULL, TRUE);
+
+    gl.argv = (const char *const *) argv;
+    gl.argc = argc;
+    gl.loop = g_main_loop_new(NULL, FALSE);
+
+    _LOGI("bluetooth test util start");
+
+    gl.argv_cmd = argc >= 2 ? argv[1] : NULL;
+
+    for (i = 0; i < G_N_ELEMENTS(main_cmd_infos); i++) {
+        if (nm_streq0(main_cmd_infos[i].name, gl.argv_cmd)) {
+            _LOGD("start \"%s\"", gl.argv_cmd);
+            exit_code = main_cmd_infos[i].main_func(&main_cmd_infos[i]);
+            _LOGD("completed with %d", exit_code);
+            break;
+        }
+    }
+    if (gl.argv_cmd && i >= G_N_ELEMENTS(main_cmd_infos)) {
+        nm_log_err(LOGD_BT, "invalid command \"%s\"", gl.argv_cmd);
+        exit_code = -1;
+    }
+
+    nm_clear_pointer(&gl.loop, g_main_loop_unref);
+
+    return exit_code;
+}
diff --git a/src/core/devices/meson.build b/src/core/devices/meson.build
new file mode 100644
index 00000000..66f7aa92
--- /dev/null
+++ b/src/core/devices/meson.build
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+subdir('adsl')
+
+if enable_modem_manager
+  subdir('wwan')
+  subdir('bluetooth')
+endif
+
+if enable_wifi
+  subdir('wifi')
+endif
+
+if enable_teamdctl
+  subdir('team')
+endif
+
+if enable_ovs
+  subdir('ovs')
+endif
+
+if enable_tests
+  subdir('tests')
+endif
diff --git a/src/core/devices/nm-acd-manager.c b/src/core/devices/nm-acd-manager.c
new file mode 100644
index 00000000..b95f90fd
--- /dev/null
+++ b/src/core/devices/nm-acd-manager.c
@@ -0,0 +1,496 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-acd-manager.h"
+
+#include <netinet/in.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <linux/if_ether.h>
+
+#include "platform/nm-platform.h"
+#include "nm-utils.h"
+#include "NetworkManagerUtils.h"
+#include "n-acd/src/n-acd.h"
+
+/*****************************************************************************/
+
+typedef enum {
+    STATE_INIT,
+    STATE_PROBING,
+    STATE_PROBE_DONE,
+    STATE_ANNOUNCING,
+} State;
+
+typedef struct {
+    in_addr_t  address;
+    gboolean   duplicate;
+    NAcdProbe *probe;
+} AddressInfo;
+
+struct _NMAcdManager {
+    int         ifindex;
+    guint8      hwaddr[ETH_ALEN];
+    State       state;
+    GHashTable *addresses;
+    guint       completed;
+    NAcd *      acd;
+    GSource *   event_source;
+
+    NMAcdCallbacks callbacks;
+    gpointer       user_data;
+};
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_IP4
+#define _NMLOG_PREFIX_NAME "acd"
+#define _NMLOG(level, ...)                                                     \
+    G_STMT_START                                                               \
+    {                                                                          \
+        char _sbuf[64];                                                        \
+                                                                               \
+        nm_log((level),                                                        \
+               _NMLOG_DOMAIN,                                                  \
+               self && self->ifindex > 0                                       \
+                   ? nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex) \
+                   : NULL,                                                     \
+               NULL,                                                           \
+               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                    \
+               _NMLOG_PREFIX_NAME,                                             \
+               self ? nm_sprintf_buf(_sbuf, "[%p,%d]", self, self->ifindex)    \
+                    : "" _NM_UTILS_MACRO_REST(__VA_ARGS__));                   \
+    }                                                                          \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static const char *
+_acd_event_to_string(unsigned int event)
+{
+    switch (event) {
+    case N_ACD_EVENT_READY:
+        return "ready";
+    case N_ACD_EVENT_USED:
+        return "used";
+    case N_ACD_EVENT_DEFENDED:
+        return "defended";
+    case N_ACD_EVENT_CONFLICT:
+        return "conflict";
+    case N_ACD_EVENT_DOWN:
+        return "down";
+    }
+    return NULL;
+}
+
+#define ACD_EVENT_TO_STRING_BUF_SIZE 50
+
+static const char *
+_acd_event_to_string_buf(unsigned event, char buffer[static ACD_EVENT_TO_STRING_BUF_SIZE])
+{
+    const char *s;
+
+    s = _acd_event_to_string(event);
+    if (s)
+        return s;
+
+    g_snprintf(buffer, ACD_EVENT_TO_STRING_BUF_SIZE, "(%u)", event);
+    return buffer;
+}
+
+static const char *
+acd_error_to_string(int error)
+{
+    if (error < 0)
+        return nm_strerror_native(-error);
+
+    switch (error) {
+    case _N_ACD_E_SUCCESS:
+        return "success";
+    case N_ACD_E_PREEMPTED:
+        return "preempted";
+    case N_ACD_E_INVALID_ARGUMENT:
+        return "invalid argument";
+    }
+
+    g_return_val_if_reached(NULL);
+}
+
+static int
+acd_error_to_nmerr(int error, gboolean always_fail)
+{
+    if (error < 0)
+        return -nm_errno_native(error);
+
+    if (always_fail) {
+        if (NM_IN_SET(error, N_ACD_E_PREEMPTED, N_ACD_E_INVALID_ARGUMENT))
+            return -NME_UNSPEC;
+        g_return_val_if_reached(-NME_UNSPEC);
+    }
+
+    /* so, @error is either zero (indicating success) or one
+     * of the special status codes like N_ACD_E_*. In both cases,
+     * return the positive value here. */
+    if (NM_IN_SET(error, _N_ACD_E_SUCCESS, N_ACD_E_PREEMPTED, N_ACD_E_INVALID_ARGUMENT))
+        return error;
+
+    g_return_val_if_reached(error);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_acd_manager_add_address:
+ * @self: a #NMAcdManager
+ * @address: an IP address
+ *
+ * Add @address to the list of IP addresses to probe.
+
+ * Returns: %TRUE on success, %FALSE if the address was already in the list
+ */
+gboolean
+nm_acd_manager_add_address(NMAcdManager *self, in_addr_t address)
+{
+    AddressInfo *info;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(self->state == STATE_INIT, FALSE);
+
+    if (g_hash_table_lookup(self->addresses, GUINT_TO_POINTER(address)))
+        return FALSE;
+
+    info          = g_slice_new0(AddressInfo);
+    info->address = address;
+
+    g_hash_table_insert(self->addresses, GUINT_TO_POINTER(address), info);
+
+    return TRUE;
+}
+
+static gboolean
+acd_event(int fd, GIOCondition condition, gpointer data)
+{
+    NMAcdManager *self = data;
+    NAcdEvent *   event;
+    AddressInfo * info;
+    gboolean      emit_probe_terminated = FALSE;
+    char          address_str[INET_ADDRSTRLEN];
+    int           r;
+
+    if (n_acd_dispatch(self->acd))
+        return G_SOURCE_CONTINUE;
+
+    while (!n_acd_pop_event(self->acd, &event) && event) {
+        char          to_string_buffer[ACD_EVENT_TO_STRING_BUF_SIZE];
+        gs_free char *hwaddr_str         = NULL;
+        gboolean      check_probing_done = FALSE;
+
+        switch (event->event) {
+        case N_ACD_EVENT_READY:
+            n_acd_probe_get_userdata(event->ready.probe, (void **) &info);
+            info->duplicate = FALSE;
+            if (self->state == STATE_ANNOUNCING) {
+                /* fake probe ended, start announcing */
+                r = n_acd_probe_announce(info->probe, N_ACD_DEFEND_ONCE);
+                if (r) {
+                    _LOGW("couldn't announce address %s on interface '%s': %s",
+                          _nm_utils_inet4_ntop(info->address, address_str),
+                          nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+                          acd_error_to_string(r));
+                } else {
+                    _LOGD("announcing address %s",
+                          _nm_utils_inet4_ntop(info->address, address_str));
+                }
+            }
+            check_probing_done = TRUE;
+            break;
+        case N_ACD_EVENT_USED:
+            n_acd_probe_get_userdata(event->used.probe, (void **) &info);
+            info->duplicate    = TRUE;
+            check_probing_done = TRUE;
+            break;
+        case N_ACD_EVENT_DEFENDED:
+            n_acd_probe_get_userdata(event->defended.probe, (void **) &info);
+            _LOGD("defended address %s from host %s",
+                  _nm_utils_inet4_ntop(info->address, address_str),
+                  (hwaddr_str =
+                       nm_utils_hwaddr_ntoa(event->defended.sender, event->defended.n_sender)));
+            break;
+        case N_ACD_EVENT_CONFLICT:
+            n_acd_probe_get_userdata(event->conflict.probe, (void **) &info);
+            _LOGW("conflict for address %s detected with host %s on interface '%s'",
+                  _nm_utils_inet4_ntop(info->address, address_str),
+                  (hwaddr_str =
+                       nm_utils_hwaddr_ntoa(event->defended.sender, event->defended.n_sender)),
+                  nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex));
+            break;
+        default:
+            _LOGD("unhandled event '%s'", _acd_event_to_string_buf(event->event, to_string_buffer));
+            break;
+        }
+
+        if (check_probing_done && self->state == STATE_PROBING
+            && ++self->completed == g_hash_table_size(self->addresses)) {
+            self->state           = STATE_PROBE_DONE;
+            emit_probe_terminated = TRUE;
+        }
+    }
+
+    if (emit_probe_terminated) {
+        if (self->callbacks.probe_terminated_callback) {
+            self->callbacks.probe_terminated_callback(self, self->user_data);
+        }
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+acd_probe_add(NMAcdManager *self, AddressInfo *info, guint64 timeout)
+{
+    NAcdProbeConfig *probe_config;
+    int              r;
+    char             sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+    r = n_acd_probe_config_new(&probe_config);
+    if (r) {
+        _LOGW("could not create probe config for %s on interface '%s': %s",
+              _nm_utils_inet4_ntop(info->address, sbuf),
+              nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+              acd_error_to_string(r));
+        return FALSE;
+    }
+
+    n_acd_probe_config_set_ip(probe_config, (struct in_addr){info->address});
+    n_acd_probe_config_set_timeout(probe_config, timeout);
+
+    r = n_acd_probe(self->acd, &info->probe, probe_config);
+    if (r) {
+        _LOGW("could not start probe for %s on interface '%s': %s",
+              _nm_utils_inet4_ntop(info->address, sbuf),
+              nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+              acd_error_to_string(r));
+        n_acd_probe_config_free(probe_config);
+        return FALSE;
+    }
+
+    n_acd_probe_set_userdata(info->probe, info);
+    n_acd_probe_config_free(probe_config);
+
+    return TRUE;
+}
+
+static int
+acd_init(NMAcdManager *self)
+{
+    NAcdConfig *config;
+    int         r;
+
+    if (self->acd)
+        return 0;
+
+    r = n_acd_config_new(&config);
+    if (r)
+        return r;
+
+    n_acd_config_set_ifindex(config, self->ifindex);
+    n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET);
+    n_acd_config_set_mac(config, self->hwaddr, ETH_ALEN);
+
+    r = n_acd_new(&self->acd, config);
+    n_acd_config_free(config);
+    return r;
+}
+
+/**
+ * nm_acd_manager_start_probe:
+ * @self: a #NMAcdManager
+ * @timeout: maximum probe duration in milliseconds
+ * @error: location to store error, or %NULL
+ *
+ * Start probing IP addresses for duplicates; when the probe terminates a
+ * PROBE_TERMINATED signal is emitted.
+ *
+ * Returns: 0 on success or a negative NetworkManager error code (NME_*).
+ */
+int
+nm_acd_manager_start_probe(NMAcdManager *self, guint timeout)
+{
+    GHashTableIter iter;
+    AddressInfo *  info;
+    gboolean       success = FALSE;
+    int            fd, r;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(self->state == STATE_INIT, FALSE);
+
+    r = acd_init(self);
+    if (r) {
+        _LOGW("couldn't init ACD for probing on interface '%s': %s",
+              nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+              acd_error_to_string(r));
+        return acd_error_to_nmerr(r, TRUE);
+    }
+
+    self->completed = 0;
+
+    g_hash_table_iter_init(&iter, self->addresses);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info))
+        success |= acd_probe_add(self, info, timeout);
+
+    if (success)
+        self->state = STATE_PROBING;
+
+    nm_assert(!self->event_source);
+    n_acd_get_fd(self->acd, &fd);
+    self->event_source =
+        nm_g_unix_fd_source_new(fd, G_IO_IN, G_PRIORITY_DEFAULT, acd_event, self, NULL);
+    g_source_attach(self->event_source, NULL);
+
+    return success ? 0 : -NME_UNSPEC;
+}
+
+/**
+ * nm_acd_manager_check_address:
+ * @self: a #NMAcdManager
+ * @address: an IP address
+ *
+ * Check if an IP address is duplicate. @address must have been added with
+ * nm_acd_manager_add_address().
+ *
+ * Returns: %TRUE if the address is not duplicate, %FALSE otherwise
+ */
+gboolean
+nm_acd_manager_check_address(NMAcdManager *self, in_addr_t address)
+{
+    AddressInfo *info;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(NM_IN_SET(self->state, STATE_INIT, STATE_PROBE_DONE), FALSE);
+
+    info = g_hash_table_lookup(self->addresses, GUINT_TO_POINTER(address));
+    g_return_val_if_fail(info, FALSE);
+
+    return !info->duplicate;
+}
+
+/**
+ * nm_acd_manager_announce_addresses:
+ * @self: a #NMAcdManager
+ *
+ * Start announcing addresses.
+ *
+ * Returns: a negative NetworkManager error number or zero on success.
+ */
+int
+nm_acd_manager_announce_addresses(NMAcdManager *self)
+{
+    GHashTableIter iter;
+    AddressInfo *  info;
+    int            r;
+    int            fd;
+    gboolean       success = TRUE;
+
+    r = acd_init(self);
+    if (r) {
+        _LOGW("couldn't init ACD for announcing addresses on interface '%s': %s",
+              nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+              acd_error_to_string(r));
+        return acd_error_to_nmerr(r, TRUE);
+    }
+
+    if (self->state == STATE_INIT) {
+        /* n-acd can't announce without probing, therefore let's
+         * start a fake probe with zero timeout and then perform
+         * the announcement. */
+        g_hash_table_iter_init(&iter, self->addresses);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info)) {
+            if (!acd_probe_add(self, info, 0))
+                success = FALSE;
+        }
+        self->state = STATE_ANNOUNCING;
+    } else if (self->state == STATE_ANNOUNCING) {
+        char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+        g_hash_table_iter_init(&iter, self->addresses);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &info)) {
+            if (info->duplicate)
+                continue;
+            r = n_acd_probe_announce(info->probe, N_ACD_DEFEND_ONCE);
+            if (r) {
+                _LOGW("couldn't announce address %s on interface '%s': %s",
+                      _nm_utils_inet4_ntop(info->address, sbuf),
+                      nm_platform_link_get_name(NM_PLATFORM_GET, self->ifindex),
+                      acd_error_to_string(r));
+                success = FALSE;
+            } else
+                _LOGD("announcing address %s", _nm_utils_inet4_ntop(info->address, sbuf));
+        }
+    }
+
+    if (!self->event_source) {
+        n_acd_get_fd(self->acd, &fd);
+        self->event_source =
+            nm_g_unix_fd_source_new(fd, G_IO_IN, G_PRIORITY_DEFAULT, acd_event, self, NULL);
+        g_source_attach(self->event_source, NULL);
+    }
+
+    return success ? 0 : -NME_UNSPEC;
+}
+
+static void
+destroy_address_info(gpointer data)
+{
+    AddressInfo *info = (AddressInfo *) data;
+
+    n_acd_probe_free(info->probe);
+
+    g_slice_free(AddressInfo, info);
+}
+
+/*****************************************************************************/
+
+NMAcdManager *
+nm_acd_manager_new(int                   ifindex,
+                   const guint8 *        hwaddr,
+                   guint                 hwaddr_len,
+                   const NMAcdCallbacks *callbacks,
+                   gpointer              user_data)
+{
+    NMAcdManager *self;
+
+    g_return_val_if_fail(ifindex > 0, NULL);
+    g_return_val_if_fail(hwaddr, NULL);
+    g_return_val_if_fail(hwaddr_len == ETH_ALEN, NULL);
+
+    self = g_slice_new0(NMAcdManager);
+
+    if (callbacks)
+        self->callbacks = *callbacks;
+    self->user_data = user_data;
+
+    self->addresses = g_hash_table_new_full(nm_direct_hash, NULL, NULL, destroy_address_info);
+    self->state     = STATE_INIT;
+    self->ifindex   = ifindex;
+    memcpy(self->hwaddr, hwaddr, ETH_ALEN);
+    return self;
+}
+
+void
+nm_acd_manager_free(NMAcdManager *self)
+{
+    g_return_if_fail(self);
+
+    if (self->callbacks.user_data_destroy)
+        self->callbacks.user_data_destroy(self->user_data);
+
+    nm_clear_pointer(&self->addresses, g_hash_table_destroy);
+    nm_clear_g_source_inst(&self->event_source);
+    nm_clear_pointer(&self->acd, n_acd_unref);
+
+    g_slice_free(NMAcdManager, self);
+}
diff --git a/src/core/devices/nm-acd-manager.h b/src/core/devices/nm-acd-manager.h
new file mode 100644
index 00000000..e8ef6f2b
--- /dev/null
+++ b/src/core/devices/nm-acd-manager.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_ACD_MANAGER__
+#define __NM_ACD_MANAGER__
+
+#include <netinet/in.h>
+
+typedef struct _NMAcdManager NMAcdManager;
+
+typedef struct {
+    void (*probe_terminated_callback)(NMAcdManager *self, gpointer user_data);
+    GDestroyNotify user_data_destroy;
+} NMAcdCallbacks;
+
+NMAcdManager *nm_acd_manager_new(int                   ifindex,
+                                 const guint8 *        hwaddr,
+                                 guint                 hwaddr_len,
+                                 const NMAcdCallbacks *callbacks,
+                                 gpointer              user_data);
+
+void nm_acd_manager_free(NMAcdManager *self);
+
+gboolean nm_acd_manager_add_address(NMAcdManager *self, in_addr_t address);
+int      nm_acd_manager_start_probe(NMAcdManager *self, guint timeout);
+gboolean nm_acd_manager_check_address(NMAcdManager *self, in_addr_t address);
+int      nm_acd_manager_announce_addresses(NMAcdManager *self);
+
+NM_AUTO_DEFINE_FCN0(NMAcdManager *, _nm_auto_free_acdmgr, nm_acd_manager_free);
+#define nm_auto_free_acdmgr nm_auto(_nm_auto_free_acdmgr)
+
+#endif /* __NM_ACD_MANAGER__ */
diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c
new file mode 100644
index 00000000..fe116dd4
--- /dev/null
+++ b/src/core/devices/nm-device-6lowpan.c
@@ -0,0 +1,339 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-6lowpan.h"
+
+#include "nm-device-private.h"
+#include "settings/nm-settings.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-6lowpan.h"
+#include "nm-utils.h"
+
+#define _NMLOG_DEVICE_TYPE NMDevice6Lowpan
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    gulong parent_state_id;
+} NMDevice6LowpanPrivate;
+
+struct _NMDevice6Lowpan {
+    NMDevice               parent;
+    NMDevice6LowpanPrivate _priv;
+};
+
+struct _NMDevice6LowpanClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDevice6Lowpan, nm_device_6lowpan, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_6LOWPAN_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDevice6Lowpan, NM_IS_DEVICE_6LOWPAN, NMDevice)
+
+/*****************************************************************************/
+
+static void
+parent_state_changed(NMDevice *          parent,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason,
+                     gpointer            user_data)
+{
+    NMDevice6Lowpan *self = NM_DEVICE_6LOWPAN(user_data);
+
+    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
+                                     NM_UNMANAGED_PARENT,
+                                     !nm_device_get_managed(parent, FALSE),
+                                     reason);
+}
+
+static void
+parent_changed_notify(NMDevice *device,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    NMDevice6Lowpan *       self = NM_DEVICE_6LOWPAN(device);
+    NMDevice6LowpanPrivate *priv = NM_DEVICE_6LOWPAN_GET_PRIVATE(self);
+
+    NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)
+        ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
+
+    /*  note that @self doesn't have to clear @parent_state_id on dispose,
+     *  because NMDevice's dispose() will unset the parent, which in turn calls
+     *  parent_changed_notify(). */
+    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
+
+    if (new_parent) {
+        priv->parent_state_id = g_signal_connect(new_parent,
+                                                 NM_DEVICE_STATE_CHANGED,
+                                                 G_CALLBACK(parent_state_changed),
+                                                 device);
+
+        /* Set parent-dependent unmanaged flag */
+        nm_device_set_unmanaged_by_flags(device,
+                                         NM_UNMANAGED_PARENT,
+                                         !nm_device_get_managed(new_parent, FALSE),
+                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
+    }
+
+    if (new_ifindex > 0) {
+        /* Recheck availability now that the parent has changed */
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED);
+    }
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *      iface = nm_device_get_iface(device);
+    NMSetting6Lowpan *s_6lowpan;
+    int               parent_ifindex;
+    int               r;
+
+    s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
+    g_return_val_if_fail(s_6lowpan, FALSE);
+
+    parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0;
+
+    if (parent_ifindex <= 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "6LoWPAN devices can not be created without a parent interface");
+        g_return_val_if_fail(!parent, FALSE);
+        return FALSE;
+    }
+
+    r = nm_platform_link_6lowpan_add(nm_device_get_platform(device),
+                                     iface,
+                                     parent_ifindex,
+                                     out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create 6lowpan interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NMDevice6Lowpan *self   = NM_DEVICE_6LOWPAN(device);
+    int              parent = 0;
+    int              ifindex;
+
+    NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)->link_changed(device, pllink);
+
+    ifindex = nm_device_get_ifindex(device);
+    if (!nm_platform_link_6lowpan_get_properties(nm_device_get_platform(device),
+                                                 ifindex,
+                                                 &parent)) {
+        _LOGW(LOGD_DEVICE, "could not get 6lowpan properties");
+        return;
+    }
+
+    nm_device_parent_set_ifindex(device, parent);
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    if (!nm_device_parent_get_device(device))
+        return FALSE;
+    return NM_DEVICE_CLASS(nm_device_6lowpan_parent_class)->is_available(device, flags);
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSetting6Lowpan *s_6lowpan;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_6LOWPAN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("6LOWPAN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
+    if (!s_6lowpan) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A '6lowpan' setting is required.");
+        return FALSE;
+    }
+
+    /* If there's no 6LoWPAN interface, no parent, and no hardware address in the
+     * settings, then there's not enough information to complete the setting.
+     */
+    if (!nm_setting_6lowpan_get_parent(s_6lowpan)
+        && !nm_device_match_parent_hwaddr(device, connection, TRUE)) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INVALID_CONNECTION,
+            "The '6lowpan' setting had no interface name, parent, or hardware address.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMSetting6Lowpan *s_6lowpan =
+        NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
+
+    if (!s_6lowpan) {
+        s_6lowpan = (NMSetting6Lowpan *) nm_setting_6lowpan_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_6lowpan);
+    }
+
+    g_object_set(
+        s_6lowpan,
+        NM_SETTING_6LOWPAN_PARENT,
+        nm_device_parent_find_for_connection(device, nm_setting_6lowpan_get_parent(s_6lowpan)),
+        NULL);
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_6lowpan_init(NMDevice6Lowpan *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_6lowpan = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_6LOWPAN,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Parent", "o", NM_DEVICE_PARENT), ), ),
+};
+
+static void
+nm_device_6lowpan_class_init(NMDevice6LowpanClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_6lowpan);
+
+    device_class->connection_type_supported        = NM_SETTING_6LOWPAN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_6LOWPAN_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_6LOWPAN);
+
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->complete_connection                    = complete_connection;
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->get_configured_mtu                     = nm_device_get_configured_mtu_for_wired;
+    device_class->link_changed                           = link_changed;
+    device_class->is_available                           = is_available;
+    device_class->parent_changed_notify                  = parent_changed_notify;
+    device_class->update_connection                      = update_connection;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_6LOWPAN_DEVICE_FACTORY (nm_6lowpan_device_factory_get_type())
+#define NM_6LOWPAN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_6LOWPAN_DEVICE_FACTORY, NM6LowpanDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_6LOWPAN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "6LoWPAN",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_6LOWPAN,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_6LOWPAN,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSetting6Lowpan *s_6lowpan;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_6LOWPAN_SETTING_NAME), NULL);
+
+    s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
+    g_assert(s_6lowpan);
+
+    return nm_setting_6lowpan_get_parent(s_6lowpan);
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    NMSetting6Lowpan *s_6lowpan;
+    const char *      ifname;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_6LOWPAN_SETTING_NAME), NULL);
+
+    s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
+    g_assert(s_6lowpan);
+
+    if (!parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+    return g_strdup(ifname);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    6LOWPAN,
+    6Lowpan,
+    6lowpan,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_6LOWPAN)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_6LOWPAN_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-6lowpan.h b/src/core/devices/nm-device-6lowpan.h
new file mode 100644
index 00000000..34ea3c8d
--- /dev/null
+++ b/src/core/devices/nm-device-6lowpan.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_6LOWPAN_H__
+#define __NETWORKMANAGER_DEVICE_6LOWPAN_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_6LOWPAN (nm_device_6lowpan_get_type())
+#define NM_DEVICE_6LOWPAN(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6Lowpan))
+#define NM_DEVICE_6LOWPAN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_6LOWPAN, NMDevice6LowpanClass))
+#define NM_IS_DEVICE_6LOWPAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_6LOWPAN))
+#define NM_IS_DEVICE_6LOWPAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_6LOWPAN))
+#define NM_DEVICE_6LOWPAN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_6LOWPAN, NMDevice6LowpanClass))
+
+typedef struct _NMDevice6Lowpan      NMDevice6Lowpan;
+typedef struct _NMDevice6LowpanClass NMDevice6LowpanClass;
+
+GType nm_device_6lowpan_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_6LOWPAN_H__ */
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
new file mode 100644
index 00000000..f68c080b
--- /dev/null
+++ b/src/core/devices/nm-device-bond.c
@@ -0,0 +1,651 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-bond.h"
+
+#include <stdlib.h>
+#include <net/if.h>
+
+#include "NetworkManagerUtils.h"
+#include "nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-core-internal.h"
+#include "nm-ip4-config.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceBond
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+#define OPTIONS_APPLY_SUBSET                                                               \
+    NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY,                         \
+        NM_SETTING_BOND_OPTION_DOWNDELAY, NM_SETTING_BOND_OPTION_ARP_INTERVAL,             \
+        NM_SETTING_BOND_OPTION_ARP_VALIDATE, NM_SETTING_BOND_OPTION_PRIMARY,               \
+        NM_SETTING_BOND_OPTION_AD_ACTOR_SYSTEM, NM_SETTING_BOND_OPTION_AD_ACTOR_SYS_PRIO,  \
+        NM_SETTING_BOND_OPTION_AD_SELECT, NM_SETTING_BOND_OPTION_AD_USER_PORT_KEY,         \
+        NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE, NM_SETTING_BOND_OPTION_ARP_ALL_TARGETS,  \
+        NM_SETTING_BOND_OPTION_FAIL_OVER_MAC, NM_SETTING_BOND_OPTION_LACP_RATE,            \
+        NM_SETTING_BOND_OPTION_LP_INTERVAL, NM_SETTING_BOND_OPTION_MIN_LINKS,              \
+        NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \
+        NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_TLB_DYNAMIC_LB,         \
+        NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY,       \
+        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP
+
+#define OPTIONS_REAPPLY_SUBSET                                                            \
+    NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY,                        \
+        NM_SETTING_BOND_OPTION_DOWNDELAY, NM_SETTING_BOND_OPTION_ARP_INTERVAL,            \
+        NM_SETTING_BOND_OPTION_ARP_VALIDATE, NM_SETTING_BOND_OPTION_PRIMARY,              \
+        NM_SETTING_BOND_OPTION_AD_ACTOR_SYSTEM, NM_SETTING_BOND_OPTION_AD_ACTOR_SYS_PRIO, \
+        NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE, NM_SETTING_BOND_OPTION_ARP_ALL_TARGETS, \
+        NM_SETTING_BOND_OPTION_FAIL_OVER_MAC, NM_SETTING_BOND_OPTION_LP_INTERVAL,         \
+        NM_SETTING_BOND_OPTION_MIN_LINKS, NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE,       \
+        NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, NM_SETTING_BOND_OPTION_RESEND_IGMP,      \
+        NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY,      \
+        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP
+
+#define OPTIONS_REAPPLY_FULL                                     \
+    OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \
+        NM_SETTING_BOND_OPTION_ARP_IP_TARGET
+
+/*****************************************************************************/
+
+struct _NMDeviceBond {
+    NMDevice parent;
+};
+
+struct _NMDeviceBondClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceBond, nm_device_bond, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingBond *s_bond;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_BOND_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Bond connection"),
+                              "bond",
+                              NULL,
+                              TRUE);
+
+    s_bond = nm_connection_get_setting_bond(connection);
+    if (!s_bond) {
+        s_bond = (NMSettingBond *) nm_setting_bond_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_bond));
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_set_bond_attr(NMDevice *device, const char *attr, const char *value)
+{
+    NMDeviceBond *self    = NM_DEVICE_BOND(device);
+    int           ifindex = nm_device_get_ifindex(device);
+    gboolean      ret;
+
+    ret =
+        nm_platform_sysctl_master_set_option(nm_device_get_platform(device), ifindex, attr, value);
+    if (!ret)
+        _LOGW(LOGD_PLATFORM, "failed to set bonding attribute '%s' to '%s'", attr, value);
+    return ret;
+}
+
+#define _set_bond_attr_take(device, attr, value)                            \
+    G_STMT_START                                                            \
+    {                                                                       \
+        gs_free char *_tmp = (value);                                       \
+                                                                            \
+        _set_bond_attr(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, _tmp); \
+    }                                                                       \
+    G_STMT_END
+
+#define _set_bond_attr_printf(device, attr, fmt, ...) \
+    _set_bond_attr_take((device), (attr), g_strdup_printf(fmt, __VA_ARGS__))
+
+static gboolean
+ignore_option(NMSettingBond *s_bond, const char *option, const char *value)
+{
+    const char *defvalue;
+
+    if (nm_streq0(option, NM_SETTING_BOND_OPTION_MIIMON)) {
+        /* The default value for miimon, when missing in the setting, is
+         * 0 if arp_interval is != 0, and 100 otherwise. So, let's ignore
+         * miimon=0 (which means that miimon is disabled) and accept any
+         * other value. Adding miimon=100 does not cause any harm.
+         */
+        defvalue = "0";
+    } else
+        defvalue = nm_setting_bond_get_option_default(s_bond, option);
+
+    return nm_streq0(value, defvalue);
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceBond * self    = NM_DEVICE_BOND(device);
+    NMSettingBond *s_bond  = nm_connection_get_setting_bond(connection);
+    int            ifindex = nm_device_get_ifindex(device);
+    NMBondMode     mode    = NM_BOND_MODE_UNKNOWN;
+    const char **  options;
+
+    if (!s_bond) {
+        s_bond = (NMSettingBond *) nm_setting_bond_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_bond);
+    }
+
+    /* Read bond options from sysfs and update the Bond setting to match */
+    options = nm_setting_bond_get_valid_options(NULL);
+    for (; options[0]; options++) {
+        const char *  option = options[0];
+        gs_free char *value  = NULL;
+        char *        p;
+
+        if (NM_IN_STRSET(option, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE))
+            continue;
+
+        value =
+            nm_platform_sysctl_master_get_option(nm_device_get_platform(device), ifindex, option);
+
+        if (value && _nm_setting_bond_get_option_type(s_bond, option) == NM_BOND_OPTION_TYPE_BOTH) {
+            p = strchr(value, ' ');
+            if (p)
+                *p = '\0';
+        }
+
+        if (mode == NM_BOND_MODE_UNKNOWN) {
+            if (value && nm_streq(option, NM_SETTING_BOND_OPTION_MODE))
+                mode = _nm_setting_bond_mode_from_string(value);
+            if (mode == NM_BOND_MODE_UNKNOWN)
+                continue;
+        }
+
+        if (!_nm_setting_bond_option_supported(option, mode))
+            continue;
+
+        if (value && value[0] && !ignore_option(s_bond, option, value)) {
+            /* Replace " " with "," for arp_ip_targets from the kernel */
+            if (nm_streq(option, NM_SETTING_BOND_OPTION_ARP_IP_TARGET)) {
+                for (p = value; *p; p++) {
+                    if (*p == ' ')
+                        *p = ',';
+                }
+            }
+
+            if (!_nm_setting_bond_validate_option(option, value, NULL))
+                _LOGT(LOGD_BOND, "cannot set invalid bond option '%s' = '%s'", option, value);
+            else
+                nm_setting_bond_add_option(s_bond, option, value);
+        }
+    }
+}
+
+static gboolean
+master_update_slave_connection(NMDevice *    self,
+                               NMDevice *    slave,
+                               NMConnection *connection,
+                               GError **     error)
+{
+    g_object_set(nm_connection_get_setting_connection(connection),
+                 NM_SETTING_CONNECTION_MASTER,
+                 nm_device_get_iface(self),
+                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_BOND_SETTING_NAME,
+                 NULL);
+    return TRUE;
+}
+
+static void
+set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new_arp_ip_target)
+{
+    gs_unref_ptrarray GPtrArray *free_list = NULL;
+    gs_free const char **        cur_strv  = NULL;
+    gs_free const char **        new_strv  = NULL;
+    gsize                        cur_len;
+    gsize                        new_len;
+    gsize                        i;
+    gsize                        j;
+
+    cur_strv = nm_utils_strsplit_set_full(cur_arp_ip_target,
+                                          NM_ASCII_SPACES,
+                                          NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
+    new_strv = nm_utils_bond_option_arp_ip_targets_split(new_arp_ip_target);
+
+    cur_len = NM_PTRARRAY_LEN(cur_strv);
+    new_len = NM_PTRARRAY_LEN(new_strv);
+
+    if (new_len > 0) {
+        for (j = 0, i = 0; i < new_len; i++) {
+            const char *s;
+            in_addr_t   a4;
+
+            s = new_strv[i];
+            if (nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &a4)) {
+                char sbuf[INET_ADDRSTRLEN];
+
+                _nm_utils_inet4_ntop(a4, sbuf);
+                if (!nm_streq(s, sbuf)) {
+                    if (!free_list)
+                        free_list = g_ptr_array_new_with_free_func(g_free);
+                    s = g_strdup(sbuf);
+                    g_ptr_array_add(free_list, (gpointer) s);
+                }
+            }
+
+            if (nm_utils_strv_find_first((char **) new_strv, i, s) < 0)
+                new_strv[j++] = s;
+        }
+        new_strv[j] = NULL;
+        new_len     = j;
+    }
+
+    if (cur_len == 0 && new_len == 0)
+        return;
+
+    if (nm_utils_strv_equal(cur_strv, new_strv))
+        return;
+
+    for (i = 0; i < cur_len; i++)
+        _set_bond_attr_printf(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, "-%s", cur_strv[i]);
+    for (i = 0; i < new_len; i++)
+        _set_bond_attr_printf(device, NM_SETTING_BOND_OPTION_ARP_IP_TARGET, "+%s", new_strv[i]);
+}
+
+/*
+ * Sets bond attribute stored in the option hashtable or
+ * the default value if no value was set.
+ */
+static void
+set_bond_attr_or_default(NMDevice *device, NMSettingBond *s_bond, const char *opt)
+{
+    NMDeviceBond *self = NM_DEVICE_BOND(device);
+    const char *  value;
+
+    value = nm_setting_bond_get_option_or_default(s_bond, opt);
+    if (!value) {
+        if (_LOGT_ENABLED(LOGD_BOND) && nm_setting_bond_get_option_by_name(s_bond, opt))
+            _LOGT(LOGD_BOND, "bond option '%s' not set as it conflicts with other options", opt);
+        return;
+    }
+
+    _set_bond_attr(device, opt, value);
+}
+
+static void
+set_bond_attrs_or_default(NMDevice *device, NMSettingBond *s_bond, const char *const *attr_v)
+{
+    nm_assert(NM_IS_DEVICE(device));
+    nm_assert(s_bond);
+    nm_assert(attr_v);
+
+    for (; *attr_v; ++attr_v)
+        set_bond_attr_or_default(device, s_bond, *attr_v);
+}
+
+static void
+set_bond_arp_ip_targets(NMDevice *device, NMSettingBond *s_bond)
+{
+    int           ifindex           = nm_device_get_ifindex(device);
+    gs_free char *cur_arp_ip_target = NULL;
+
+    /* ARP targets: clear and initialize the list */
+    cur_arp_ip_target = nm_platform_sysctl_master_get_option(nm_device_get_platform(device),
+                                                             ifindex,
+                                                             NM_SETTING_BOND_OPTION_ARP_IP_TARGET);
+    set_arp_targets(
+        device,
+        cur_arp_ip_target,
+        nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_ARP_IP_TARGET));
+}
+
+static gboolean
+apply_bonding_config(NMDeviceBond *self)
+{
+    NMDevice *     device = NM_DEVICE(self);
+    NMSettingBond *s_bond;
+    NMBondMode     mode;
+    const char *   mode_str;
+    gs_free char * device_bond_mode = NULL;
+
+    s_bond = nm_device_get_applied_setting(device, NM_TYPE_SETTING_BOND);
+    g_return_val_if_fail(s_bond, FALSE);
+
+    mode_str = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE);
+    mode     = _nm_setting_bond_mode_from_string(mode_str);
+    g_return_val_if_fail(mode != NM_BOND_MODE_UNKNOWN, FALSE);
+
+    /* Set mode first, as some other options (e.g. arp_interval) are valid
+     * only for certain modes.
+     */
+    device_bond_mode = nm_platform_sysctl_master_get_option(nm_device_get_platform(device),
+                                                            nm_device_get_ifindex(device),
+                                                            NM_SETTING_BOND_OPTION_MODE);
+    /* Need to release all slaves before we can change bond mode */
+    if (!nm_streq0(device_bond_mode, mode_str))
+        nm_device_master_release_slaves(device);
+
+    set_bond_attr_or_default(device, s_bond, NM_SETTING_BOND_OPTION_MODE);
+
+    set_bond_arp_ip_targets(device, s_bond);
+
+    set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_APPLY_SUBSET));
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBond *   self = NM_DEVICE_BOND(device);
+    NMActStageReturn ret  = NM_ACT_STAGE_RETURN_SUCCESS;
+
+    /* Interface must be down to set bond options */
+    nm_device_take_down(device, TRUE);
+    if (!apply_bonding_config(self))
+        ret = NM_ACT_STAGE_RETURN_FAILURE;
+    else {
+        if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE))
+            ret = NM_ACT_STAGE_RETURN_FAILURE;
+    }
+    nm_device_bring_up(device, TRUE, NULL);
+
+    return ret;
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    NMDeviceBond *self = NM_DEVICE_BOND(device);
+
+    nm_device_master_check_slave_physical_port(device, slave, LOGD_BOND);
+
+    if (configure) {
+        gboolean success;
+
+        nm_device_take_down(slave, TRUE);
+        success = nm_platform_link_enslave(nm_device_get_platform(device),
+                                           nm_device_get_ip_ifindex(device),
+                                           nm_device_get_ip_ifindex(slave));
+        nm_device_bring_up(slave, TRUE, NULL);
+
+        if (!success) {
+            _LOGI(LOGD_BOND, "enslaved bond slave %s: failed", nm_device_get_ip_iface(slave));
+            return FALSE;
+        }
+
+        _LOGI(LOGD_BOND, "enslaved bond slave %s", nm_device_get_ip_iface(slave));
+    } else
+        _LOGI(LOGD_BOND, "bond slave %s was enslaved", nm_device_get_ip_iface(slave));
+
+    return TRUE;
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{
+    NMDeviceBond *self = NM_DEVICE_BOND(device);
+    gboolean      success;
+    gs_free char *address = NULL;
+    int           ifindex_slave;
+    int           ifindex;
+
+    if (configure) {
+        ifindex = nm_device_get_ifindex(device);
+        if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex))
+            configure = FALSE;
+    }
+
+    ifindex_slave = nm_device_get_ip_ifindex(slave);
+
+    if (ifindex_slave <= 0)
+        _LOGD(LOGD_BOND, "bond slave %s is already released", nm_device_get_ip_iface(slave));
+
+    if (configure) {
+        /* When the last slave is released the bond MAC will be set to a random
+         * value by kernel; remember the current one and restore it afterwards.
+         */
+        address = g_strdup(nm_device_get_hw_address(device));
+
+        if (ifindex_slave > 0) {
+            success = nm_platform_link_release(nm_device_get_platform(device),
+                                               nm_device_get_ip_ifindex(device),
+                                               ifindex_slave);
+
+            if (success) {
+                _LOGI(LOGD_BOND, "released bond slave %s", nm_device_get_ip_iface(slave));
+            } else {
+                _LOGW(LOGD_BOND, "failed to release bond slave %s", nm_device_get_ip_iface(slave));
+            }
+        }
+
+        nm_platform_process_events(nm_device_get_platform(device));
+        if (nm_device_update_hw_address(device))
+            nm_device_hw_addr_set(device, address, "restore", FALSE);
+
+        /* Kernel bonding code "closes" the slave when releasing it, (which clears
+         * IFF_UP), so we must bring it back up here to ensure carrier changes and
+         * other state is noticed by the now-released slave.
+         */
+        if (ifindex_slave > 0) {
+            if (!nm_device_bring_up(slave, TRUE, NULL))
+                _LOGW(LOGD_BOND, "released bond slave could not be brought up.");
+        }
+    } else {
+        if (ifindex_slave > 0) {
+            _LOGI(LOGD_BOND, "bond slave %s was released", nm_device_get_ip_iface(slave));
+        }
+    }
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *iface = nm_device_get_iface(device);
+    int         r;
+
+    g_assert(iface);
+
+    r = nm_platform_link_bond_add(nm_device_get_platform(device), iface, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create bond interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    /* Only handle bond setting here, delegate other settings to parent class */
+    if (nm_streq(setting_name, NM_SETTING_BOND_SETTING_NAME)) {
+        NMSettingBond *s_a = NM_SETTING_BOND(s_old);
+        NMSettingBond *s_b = NM_SETTING_BOND(s_new);
+        const char **  option_list;
+
+        if (!nm_device_hash_check_invalid_keys(diffs,
+                                               NM_SETTING_BOND_SETTING_NAME,
+                                               error,
+                                               NM_SETTING_BOND_OPTIONS))
+            return FALSE;
+
+        option_list = nm_setting_bond_get_valid_options(NULL);
+
+        for (; *option_list; ++option_list) {
+            const char *name = *option_list;
+
+            /* We support changes to these */
+            if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL))
+                continue;
+
+            /* Reject any other changes */
+            if (!nm_streq0(nm_setting_bond_get_option_normalized(s_a, name),
+                           nm_setting_bond_get_option_normalized(s_b, name))) {
+                g_set_error(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                            "Can't reapply '%s' bond option",
+                            name);
+                return FALSE;
+            }
+        }
+
+        return TRUE;
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_bond_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static void
+reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceBond * self = NM_DEVICE_BOND(device);
+    NMSettingBond *s_bond;
+    const char *   value;
+    NMBondMode     mode;
+
+    NM_DEVICE_CLASS(nm_device_bond_parent_class)->reapply_connection(device, con_old, con_new);
+
+    _LOGD(LOGD_BOND, "reapplying bond settings");
+    s_bond = nm_connection_get_setting_bond(con_new);
+    g_return_if_fail(s_bond);
+
+    value = nm_setting_bond_get_option_or_default(s_bond, NM_SETTING_BOND_OPTION_MODE);
+    mode  = _nm_setting_bond_mode_from_string(value);
+    g_return_if_fail(mode != NM_BOND_MODE_UNKNOWN);
+
+    /* Below we set only the bond options that kernel allows to modify
+     * while keeping the bond interface up */
+
+    set_bond_arp_ip_targets(device, s_bond);
+
+    set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_REAPPLY_SUBSET));
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_bond_init(NMDeviceBond *self)
+{
+    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_bond = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_BOND,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves",
+                                                             "ao",
+                                                             NM_DEVICE_SLAVES), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_bond_class_init(NMDeviceBondClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bond);
+
+    device_class->connection_type_supported        = NM_SETTING_BOND_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_BOND_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BOND);
+
+    device_class->is_master                = TRUE;
+    device_class->get_generic_capabilities = get_generic_capabilities;
+    device_class->complete_connection      = complete_connection;
+
+    device_class->update_connection              = update_connection;
+    device_class->master_update_slave_connection = master_update_slave_connection;
+
+    device_class->create_and_realize = create_and_realize;
+    device_class->act_stage1_prepare = act_stage1_prepare;
+    device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
+    device_class->enslave_slave      = enslave_slave;
+    device_class->release_slave      = release_slave;
+    device_class->can_reapply_change = can_reapply_change;
+    device_class->reapply_connection = reapply_connection;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_BOND_DEVICE_FACTORY (nm_bond_device_factory_get_type())
+#define NM_BOND_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BOND_DEVICE_FACTORY, NMBondDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_BOND,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_DRIVER,
+                        "bonding",
+                        NM_DEVICE_TYPE_DESC,
+                        "Bond",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_BOND,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_BOND,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    BOND,
+    Bond,
+    bond,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_BOND)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BOND_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-bond.h b/src/core/devices/nm-device-bond.h
new file mode 100644
index 00000000..19301793
--- /dev/null
+++ b/src/core/devices/nm-device-bond.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_BOND_H__
+#define __NETWORKMANAGER_DEVICE_BOND_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_BOND (nm_device_bond_get_type())
+#define NM_DEVICE_BOND(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BOND, NMDeviceBond))
+#define NM_DEVICE_BOND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BOND, NMDeviceBondClass))
+#define NM_IS_DEVICE_BOND(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BOND))
+#define NM_IS_DEVICE_BOND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BOND))
+#define NM_DEVICE_BOND_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BOND, NMDeviceBondClass))
+
+typedef struct _NMDeviceBond      NMDeviceBond;
+typedef struct _NMDeviceBondClass NMDeviceBondClass;
+
+GType nm_device_bond_get_type(void);
+
+#endif /* NM_DEVICE_BOND_H */
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
new file mode 100644
index 00000000..c919d85d
--- /dev/null
+++ b/src/core/devices/nm-device-bridge.c
@@ -0,0 +1,1246 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-bridge.h"
+
+#include <stdlib.h>
+#include <linux/if_ether.h>
+
+#include "NetworkManagerUtils.h"
+#include "nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceBridge
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceBridge {
+    NMDevice      parent;
+    GCancellable *bt_cancellable;
+    bool          vlan_configured : 1;
+    bool          bt_registered : 1;
+};
+
+struct _NMDeviceBridgeClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceBridge, nm_device_bridge, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+const NMBtVTableNetworkServer *nm_bt_vtable_network_server = NULL;
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceBridge *    self = NM_DEVICE_BRIDGE(device);
+    NMSettingBluetooth *s_bt;
+
+    if (!NM_DEVICE_CLASS(nm_device_bridge_parent_class)
+             ->check_connection_available(device, connection, flags, specific_object, error))
+        return FALSE;
+
+    s_bt = _nm_connection_get_setting_bluetooth_for_nap(connection);
+    if (s_bt) {
+        const char *bdaddr;
+
+        if (!nm_bt_vtable_network_server) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "bluetooth plugin not available to activate NAP profile");
+            return FALSE;
+        }
+
+        bdaddr = nm_setting_bluetooth_get_bdaddr(s_bt);
+        if (!nm_bt_vtable_network_server->is_available(
+                nm_bt_vtable_network_server,
+                bdaddr,
+                (self->bt_cancellable || self->bt_registered) ? device : NULL)) {
+            if (bdaddr)
+                nm_utils_error_set(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "no suitable NAP device \"%s\" available",
+                                   bdaddr);
+            else
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "no suitable NAP device available");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingBridge *s_bridge;
+    const char *     mac_address;
+
+    if (!NM_DEVICE_CLASS(nm_device_bridge_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME)
+        && _nm_connection_get_setting_bluetooth_for_nap(connection)) {
+        s_bridge = nm_connection_get_setting_bridge(connection);
+        if (!s_bridge) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "missing bridge setting for bluetooth NAP profile");
+            return FALSE;
+        }
+
+        /* a bluetooth NAP connection is handled by the bridge.
+         *
+         * Proceed... */
+    } else {
+        s_bridge =
+            _nm_connection_check_main_setting(connection, NM_SETTING_BRIDGE_SETTING_NAME, error);
+        if (!s_bridge)
+            return FALSE;
+    }
+
+    mac_address = nm_setting_bridge_get_mac_address(s_bridge);
+    if (mac_address && nm_device_is_real(device)) {
+        const char *hw_addr;
+
+        hw_addr = nm_device_get_hw_address(device);
+        if (!hw_addr || !nm_utils_hwaddr_matches(hw_addr, -1, mac_address, -1)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "mac address mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingBridge *s_bridge;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_BRIDGE_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Bridge connection"),
+                              "bridge",
+                              NULL,
+                              TRUE);
+
+    s_bridge = nm_connection_get_setting_bridge(connection);
+    if (!s_bridge) {
+        s_bridge = (NMSettingBridge *) nm_setting_bridge_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_bridge));
+    }
+
+    return TRUE;
+}
+
+static void
+to_sysfs_group_address_sys(const char *group_address, NMEtherAddr *out_addr)
+{
+    if (group_address == NULL) {
+        *out_addr = NM_ETHER_ADDR_INIT(NM_BRIDGE_GROUP_ADDRESS_DEF_BIN);
+        return;
+    }
+    if (!nm_utils_hwaddr_aton(group_address, out_addr, ETH_ALEN))
+        nm_assert_not_reached();
+}
+
+static void
+from_sysfs_group_address(const char *value, GValue *out)
+{
+    if (!nm_utils_hwaddr_matches(value, -1, NM_BRIDGE_GROUP_ADDRESS_DEF_STR, -1))
+        g_value_set_string(out, value);
+}
+
+static const char *
+to_sysfs_group_address(GValue *value)
+{
+    return g_value_get_string(value) ?: NM_BRIDGE_GROUP_ADDRESS_DEF_STR;
+}
+
+static int
+to_sysfs_vlan_protocol_sys(const char *value)
+{
+    if (nm_streq0(value, "802.1ad"))
+        return ETH_P_8021AD;
+
+    return ETH_P_8021Q;
+}
+
+static void
+from_sysfs_vlan_protocol(const char *value, GValue *out)
+{
+    switch (_nm_utils_ascii_str_to_uint64(value, 16, 0, G_MAXUINT, -1)) {
+    case ETH_P_8021Q:
+        /* default value */
+        break;
+    case ETH_P_8021AD:
+        g_value_set_string(out, "802.1ad");
+        break;
+    }
+}
+
+static const char *
+to_sysfs_vlan_protocol(GValue *value)
+{
+    const char *str = g_value_get_string(value);
+
+    if (nm_streq0(str, "802.1ad")) {
+        G_STATIC_ASSERT_EXPR(ETH_P_8021AD == 0x88A8);
+        return "0x88A8";
+    }
+
+    G_STATIC_ASSERT_EXPR(ETH_P_8021Q == 0x8100);
+    return "0x8100";
+}
+
+static int
+to_sysfs_multicast_router_sys(const char *value)
+{
+    if (nm_streq0(value, "disabled"))
+        return 0;
+    if (nm_streq0(value, "auto"))
+        return 1;
+    if (nm_streq0(value, "enabled"))
+        return 2;
+
+    return 1;
+}
+
+static const char *
+to_sysfs_multicast_router(GValue *value)
+{
+    const char *str = g_value_get_string(value);
+
+    if (nm_streq0(str, "disabled"))
+        return "0";
+    if (nm_streq0(str, "auto"))
+        return "1";
+    if (nm_streq0(str, "enabled"))
+        return "2";
+
+    return "1";
+}
+
+static void
+from_sysfs_multicast_router(const char *value, GValue *out)
+{
+    switch (_nm_utils_ascii_str_to_uint64(value, 10, 0, G_MAXUINT, -1)) {
+    case 0:
+        g_value_set_string(out, "disabled");
+        break;
+    case 2:
+        g_value_set_string(out, "enabled");
+        break;
+    case 1:
+    default:
+        /* default value */
+        break;
+    }
+}
+
+/*****************************************************************************/
+#define _DEFAULT_IF_ZERO(val, def_val)    \
+    ({                                    \
+        typeof(val) _val     = (val);     \
+        typeof(val) _def_val = (def_val); \
+                                          \
+        (_val == 0) ? _def_val : _val;    \
+    })
+
+typedef struct {
+    const char *name;
+    const char *sysname;
+    const char *(*to_sysfs)(GValue *value);
+    void (*from_sysfs)(const char *value, GValue *out);
+    guint64 nm_min;
+    guint64 nm_max;
+    guint64 nm_default;
+    bool    default_if_zero;
+    bool    user_hz_compensate;
+    bool    only_with_stp;
+} Option;
+
+#define OPTION(_name, _sysname, ...) \
+    {                                \
+        .name = ""_name              \
+                "",                  \
+        .sysname = ""_sysname        \
+                   "",               \
+        __VA_ARGS__                  \
+    }
+
+#define OPTION_TYPE_INT(min, max, def) .nm_min = (min), .nm_max = (max), .nm_default = (def)
+
+#define OPTION_TYPE_BOOL(def) OPTION_TYPE_INT(FALSE, TRUE, def)
+
+#define OPTION_TYPE_TOFROM(to, fro) .to_sysfs = (to), .from_sysfs = (fro)
+
+static const Option master_options[] = {
+    OPTION(NM_SETTING_BRIDGE_STP, /* this must stay as the first item */
+           "stp_state",
+           OPTION_TYPE_BOOL(NM_BRIDGE_STP_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_PRIORITY,
+           "priority",
+           OPTION_TYPE_INT(NM_BRIDGE_PRIORITY_MIN, NM_BRIDGE_PRIORITY_MAX, NM_BRIDGE_PRIORITY_DEF),
+           .default_if_zero = TRUE,
+           .only_with_stp   = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_FORWARD_DELAY,
+           "forward_delay",
+           OPTION_TYPE_INT(NM_BRIDGE_FORWARD_DELAY_MIN,
+                           NM_BRIDGE_FORWARD_DELAY_MAX,
+                           NM_BRIDGE_FORWARD_DELAY_DEF),
+           .default_if_zero    = TRUE,
+           .user_hz_compensate = TRUE,
+           .only_with_stp      = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_HELLO_TIME,
+           "hello_time",
+           OPTION_TYPE_INT(NM_BRIDGE_HELLO_TIME_MIN,
+                           NM_BRIDGE_HELLO_TIME_MAX,
+                           NM_BRIDGE_HELLO_TIME_DEF),
+           .default_if_zero    = TRUE,
+           .user_hz_compensate = TRUE,
+           .only_with_stp      = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_MAX_AGE,
+           "max_age",
+           OPTION_TYPE_INT(NM_BRIDGE_MAX_AGE_MIN, NM_BRIDGE_MAX_AGE_MAX, NM_BRIDGE_MAX_AGE_DEF),
+           .default_if_zero    = TRUE,
+           .user_hz_compensate = TRUE,
+           .only_with_stp      = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_AGEING_TIME,
+           "ageing_time",
+           OPTION_TYPE_INT(NM_BRIDGE_AGEING_TIME_MIN,
+                           NM_BRIDGE_AGEING_TIME_MAX,
+                           NM_BRIDGE_AGEING_TIME_DEF),
+           .default_if_zero    = TRUE,
+           .user_hz_compensate = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_GROUP_FORWARD_MASK, "group_fwd_mask", OPTION_TYPE_INT(0, 0xFFFF, 0), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_HASH_MAX,
+           "hash_max",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_HASH_MAX_MIN,
+                           NM_BRIDGE_MULTICAST_HASH_MAX_MAX,
+                           NM_BRIDGE_MULTICAST_HASH_MAX_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_COUNT,
+           "multicast_last_member_count",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MIN,
+                           NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_MAX,
+                           NM_BRIDGE_MULTICAST_LAST_MEMBER_COUNT_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL,
+           "multicast_last_member_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_LAST_MEMBER_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL,
+           "multicast_membership_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_MEMBERSHIP_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERIER,
+           "multicast_querier",
+           OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_QUERIER_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERIER_INTERVAL,
+           "multicast_querier_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_QUERIER_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_INTERVAL,
+           "multicast_query_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_QUERY_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_QUERY_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL,
+           "multicast_query_response_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_QUERY_RESPONSE_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_QUERY_USE_IFADDR,
+           "multicast_query_use_ifaddr",
+           OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_QUERY_USE_IFADDR_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_SNOOPING,
+           "multicast_snooping",
+           OPTION_TYPE_BOOL(NM_BRIDGE_MULTICAST_SNOOPING_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_ROUTER,
+           "multicast_router",
+           OPTION_TYPE_TOFROM(to_sysfs_multicast_router, from_sysfs_multicast_router), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT,
+           "multicast_startup_query_count",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MIN,
+                           NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_MAX,
+                           NM_BRIDGE_MULTICAST_STARTUP_QUERY_COUNT_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL,
+           "multicast_startup_query_interval",
+           OPTION_TYPE_INT(NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MIN,
+                           NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_MAX,
+                           NM_BRIDGE_MULTICAST_STARTUP_QUERY_INTERVAL_DEF), ),
+    OPTION(NM_SETTING_BRIDGE_GROUP_ADDRESS,
+           "group_addr",
+           OPTION_TYPE_TOFROM(to_sysfs_group_address, from_sysfs_group_address), ),
+    OPTION(NM_SETTING_BRIDGE_VLAN_PROTOCOL,
+           "vlan_protocol",
+           OPTION_TYPE_TOFROM(to_sysfs_vlan_protocol, from_sysfs_vlan_protocol), ),
+    OPTION(NM_SETTING_BRIDGE_VLAN_STATS_ENABLED,
+           "vlan_stats_enabled",
+           OPTION_TYPE_BOOL(NM_BRIDGE_VLAN_STATS_ENABLED_DEF)),
+    {
+        0,
+    }};
+
+static const Option slave_options[] = {
+    OPTION(NM_SETTING_BRIDGE_PORT_PRIORITY,
+           "priority",
+           OPTION_TYPE_INT(NM_BRIDGE_PORT_PRIORITY_MIN,
+                           NM_BRIDGE_PORT_PRIORITY_MAX,
+                           NM_BRIDGE_PORT_PRIORITY_DEF),
+           .default_if_zero = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_PORT_PATH_COST,
+           "path_cost",
+           OPTION_TYPE_INT(NM_BRIDGE_PORT_PATH_COST_MIN,
+                           NM_BRIDGE_PORT_PATH_COST_MAX,
+                           NM_BRIDGE_PORT_PATH_COST_DEF),
+           .default_if_zero = TRUE, ),
+    OPTION(NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE, "hairpin_mode", OPTION_TYPE_BOOL(FALSE), ),
+    {0}};
+
+static void
+commit_option(NMDevice *device, NMSetting *setting, const Option *option, gboolean slave)
+{
+    int                         ifindex = nm_device_get_ifindex(device);
+    nm_auto_unset_gvalue GValue val     = G_VALUE_INIT;
+    GParamSpec *                pspec;
+    const char *                value;
+    char                        value_buf[100];
+
+    if (slave)
+        nm_assert(NM_IS_SETTING_BRIDGE_PORT(setting));
+    else
+        nm_assert(NM_IS_SETTING_BRIDGE(setting));
+
+    pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), option->name);
+    nm_assert(pspec);
+
+    g_value_init(&val, G_PARAM_SPEC_VALUE_TYPE(pspec));
+    g_object_get_property((GObject *) setting, option->name, &val);
+
+    if (option->to_sysfs) {
+        value = option->to_sysfs(&val);
+        goto out;
+    }
+
+    switch (pspec->value_type) {
+    case G_TYPE_BOOLEAN:
+        value = g_value_get_boolean(&val) ? "1" : "0";
+        break;
+    case G_TYPE_UINT64:
+    case G_TYPE_UINT:
+    {
+        guint64 uval;
+
+        if (pspec->value_type == G_TYPE_UINT64)
+            uval = g_value_get_uint64(&val);
+        else
+            uval = (guint) g_value_get_uint(&val);
+
+        /* zero means "unspecified" for some NM properties but isn't in the
+             * allowed kernel range, so reset the property to the default value.
+             */
+        if (option->default_if_zero && uval == 0) {
+            if (pspec->value_type == G_TYPE_UINT64)
+                uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT64(pspec);
+            else
+                uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT(pspec);
+        }
+
+        /* Linux kernel bridge interfaces use 'centiseconds' for time-based values.
+             * In reality it's not centiseconds, but depends on HZ and USER_HZ, which
+             * is almost always works out to be a multiplier of 100, so we can assume
+             * centiseconds.  See clock_t_to_jiffies().
+             */
+        if (option->user_hz_compensate)
+            uval *= 100;
+
+        if (pspec->value_type == G_TYPE_UINT64)
+            nm_sprintf_buf(value_buf, "%" G_GUINT64_FORMAT, uval);
+        else
+            nm_sprintf_buf(value_buf, "%u", (guint) uval);
+
+        value = value_buf;
+    } break;
+    case G_TYPE_STRING:
+        value = g_value_get_string(&val);
+        break;
+    default:
+        nm_assert_not_reached();
+        value = NULL;
+        break;
+    }
+
+out:
+    if (!value)
+        return;
+
+    if (slave) {
+        nm_platform_sysctl_slave_set_option(nm_device_get_platform(device),
+                                            ifindex,
+                                            option->sysname,
+                                            value);
+    } else {
+        nm_platform_sysctl_master_set_option(nm_device_get_platform(device),
+                                             ifindex,
+                                             option->sysname,
+                                             value);
+    }
+}
+
+static const NMPlatformBridgeVlan **
+setting_vlans_to_platform(GPtrArray *array)
+{
+    NMPlatformBridgeVlan **arr;
+    NMPlatformBridgeVlan * p_data;
+    guint                  i;
+
+    if (!array || !array->len)
+        return NULL;
+
+    G_STATIC_ASSERT_EXPR(_nm_alignof(NMPlatformBridgeVlan *) >= _nm_alignof(NMPlatformBridgeVlan));
+    arr    = g_malloc((sizeof(NMPlatformBridgeVlan *) * (array->len + 1))
+                   + (sizeof(NMPlatformBridgeVlan) * (array->len)));
+    p_data = (NMPlatformBridgeVlan *) &arr[array->len + 1];
+
+    for (i = 0; i < array->len; i++) {
+        NMBridgeVlan *vlan = array->pdata[i];
+        guint16       vid_start, vid_end;
+
+        nm_bridge_vlan_get_vid_range(vlan, &vid_start, &vid_end);
+
+        p_data[i] = (NMPlatformBridgeVlan){
+            .vid_start = vid_start,
+            .vid_end   = vid_end,
+            .pvid      = nm_bridge_vlan_is_pvid(vlan),
+            .untagged  = nm_bridge_vlan_is_untagged(vlan),
+        };
+        arr[i] = &p_data[i];
+    }
+    arr[i] = NULL;
+    return (const NMPlatformBridgeVlan **) arr;
+}
+
+static void
+commit_slave_options(NMDevice *device, NMSettingBridgePort *setting)
+{
+    const Option *  option;
+    NMSetting *     s;
+    gs_unref_object NMSetting *s_clear = NULL;
+
+    if (setting)
+        s = NM_SETTING(setting);
+    else
+        s = s_clear = nm_setting_bridge_port_new();
+
+    for (option = slave_options; option->name; option++)
+        commit_option(device, s, option, TRUE);
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceBridge * self     = NM_DEVICE_BRIDGE(device);
+    NMSettingBridge *s_bridge = nm_connection_get_setting_bridge(connection);
+    int              ifindex  = nm_device_get_ifindex(device);
+    const Option *   option;
+    gs_free char *   stp = NULL;
+    int              stp_value;
+
+    if (!s_bridge) {
+        s_bridge = (NMSettingBridge *) nm_setting_bridge_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_bridge);
+    }
+
+    option = master_options;
+    nm_assert(nm_streq(option->sysname, "stp_state"));
+
+    stp = nm_platform_sysctl_master_get_option(nm_device_get_platform(device),
+                                               ifindex,
+                                               option->sysname);
+    stp_value =
+        _nm_utils_ascii_str_to_int64(stp, 10, option->nm_min, option->nm_max, option->nm_default);
+    g_object_set(s_bridge, option->name, stp_value, NULL);
+    option++;
+
+    for (; option->name; option++) {
+        nm_auto_unset_gvalue GValue value = G_VALUE_INIT;
+        gs_free char *              str   = NULL;
+        GParamSpec *                pspec;
+
+        str   = nm_platform_sysctl_master_get_option(nm_device_get_platform(device),
+                                                   ifindex,
+                                                   option->sysname);
+        pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(s_bridge), option->name);
+
+        if (!stp_value && option->only_with_stp)
+            continue;
+
+        if (!str) {
+            _LOGW(LOGD_BRIDGE, "failed to read bridge setting '%s'", option->sysname);
+            continue;
+        }
+
+        g_value_init(&value, G_PARAM_SPEC_VALUE_TYPE(pspec));
+
+        if (option->from_sysfs) {
+            option->from_sysfs(str, &value);
+            goto out;
+        }
+
+        switch (pspec->value_type) {
+        case G_TYPE_UINT64:
+        case G_TYPE_UINT:
+        {
+            guint64 uvalue;
+
+            /* See comments in set_sysfs_uint() about centiseconds. */
+            if (option->user_hz_compensate) {
+                uvalue = _nm_utils_ascii_str_to_int64(str,
+                                                      10,
+                                                      option->nm_min * 100,
+                                                      option->nm_max * 100,
+                                                      option->nm_default * 100);
+                uvalue /= 100;
+            } else {
+                uvalue = _nm_utils_ascii_str_to_uint64(str,
+                                                       10,
+                                                       option->nm_min,
+                                                       option->nm_max,
+                                                       option->nm_default);
+            }
+
+            if (pspec->value_type == G_TYPE_UINT64)
+                g_value_set_uint64(&value, uvalue);
+            else
+                g_value_set_uint(&value, (guint) uvalue);
+        } break;
+        case G_TYPE_BOOLEAN:
+        {
+            gboolean bvalue;
+
+            bvalue = _nm_utils_ascii_str_to_int64(str,
+                                                  10,
+                                                  option->nm_min,
+                                                  option->nm_max,
+                                                  option->nm_default);
+            g_value_set_boolean(&value, bvalue);
+        } break;
+        case G_TYPE_STRING:
+            g_value_set_string(&value, str);
+            break;
+        default:
+            nm_assert_not_reached();
+            break;
+        }
+
+out:
+        g_object_set_property(G_OBJECT(s_bridge), option->name, &value);
+    }
+}
+
+static gboolean
+master_update_slave_connection(NMDevice *    device,
+                               NMDevice *    slave,
+                               NMConnection *connection,
+                               GError **     error)
+{
+    NMDeviceBridge *     self = NM_DEVICE_BRIDGE(device);
+    NMSettingConnection *s_con;
+    NMSettingBridgePort *s_port;
+    int                  ifindex_slave = nm_device_get_ifindex(slave);
+    const char *         iface         = nm_device_get_iface(device);
+    const Option *       option;
+
+    g_return_val_if_fail(ifindex_slave > 0, FALSE);
+
+    s_con  = nm_connection_get_setting_connection(connection);
+    s_port = nm_connection_get_setting_bridge_port(connection);
+    if (!s_port) {
+        s_port = (NMSettingBridgePort *) nm_setting_bridge_port_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_port));
+    }
+
+    for (option = slave_options; option->name; option++) {
+        gs_free char *str = nm_platform_sysctl_slave_get_option(nm_device_get_platform(device),
+                                                                ifindex_slave,
+                                                                option->sysname);
+        uint          value;
+
+        if (str) {
+            /* See comments in set_sysfs_uint() about centiseconds. */
+            if (option->user_hz_compensate) {
+                value = _nm_utils_ascii_str_to_int64(str,
+                                                     10,
+                                                     option->nm_min * 100,
+                                                     option->nm_max * 100,
+                                                     option->nm_default * 100);
+                value /= 100;
+            } else {
+                value = _nm_utils_ascii_str_to_int64(str,
+                                                     10,
+                                                     option->nm_min,
+                                                     option->nm_max,
+                                                     option->nm_default);
+            }
+            g_object_set(s_port, option->name, value, NULL);
+        } else
+            _LOGW(LOGD_BRIDGE, "failed to read bridge port setting '%s'", option->sysname);
+    }
+
+    g_object_set(s_con,
+                 NM_SETTING_CONNECTION_MASTER,
+                 iface,
+                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_BRIDGE_SETTING_NAME,
+                 NULL);
+    return TRUE;
+}
+
+static gboolean
+bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge)
+{
+    NMDeviceBridge *  self = NM_DEVICE_BRIDGE(device);
+    gconstpointer     hwaddr;
+    size_t            length;
+    gboolean          enabled;
+    guint16           pvid;
+    NMPlatform *      plat;
+    int               ifindex;
+    gs_unref_ptrarray GPtrArray *vlans              = NULL;
+    gs_free const NMPlatformBridgeVlan **plat_vlans = NULL;
+
+    if (self->vlan_configured)
+        return TRUE;
+
+    plat    = nm_device_get_platform(device);
+    ifindex = nm_device_get_ifindex(device);
+    enabled = nm_setting_bridge_get_vlan_filtering(s_bridge);
+
+    if (!enabled) {
+        nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0");
+        nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "1");
+        nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL);
+        return TRUE;
+    }
+
+    hwaddr = nm_platform_link_get_address(plat, ifindex, &length);
+    g_return_val_if_fail(length == ETH_ALEN, FALSE);
+    if (nm_utils_hwaddr_matches(hwaddr, length, &nm_ether_addr_zero, ETH_ALEN)) {
+        /* We need a non-zero MAC address to set the default pvid.
+         * Retry later. */
+        return TRUE;
+    }
+
+    self->vlan_configured = TRUE;
+
+    /* Filtering must be disabled to change the default PVID */
+    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0"))
+        return FALSE;
+
+    /* Clear the default PVID so that we later can force the re-creation of
+     * default PVID VLANs by writing the option again. */
+    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "0"))
+        return FALSE;
+
+    /* Clear all existing VLANs */
+    if (!nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL))
+        return FALSE;
+
+    /* Now set the default PVID. After this point the kernel creates
+     * a PVID VLAN on each port, including the bridge itself. */
+    pvid = nm_setting_bridge_get_vlan_default_pvid(s_bridge);
+    if (pvid) {
+        char value[32];
+
+        nm_sprintf_buf(value, "%u", pvid);
+        if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", value))
+            return FALSE;
+    }
+
+    /* Create VLANs only after setting the default PVID, so that
+     * any PVID VLAN overrides the bridge's default PVID. */
+    g_object_get(s_bridge, NM_SETTING_BRIDGE_VLANS, &vlans, NULL);
+    plat_vlans = setting_vlans_to_platform(vlans);
+    if (plat_vlans && !nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, plat_vlans))
+        return FALSE;
+
+    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "1"))
+        return FALSE;
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMConnection *connection;
+    NMSetting *   s_bridge;
+    const Option *option;
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_bridge = (NMSetting *) nm_connection_get_setting_bridge(connection);
+    g_return_val_if_fail(s_bridge, NM_ACT_STAGE_RETURN_FAILURE);
+
+    for (option = master_options; option->name; option++)
+        commit_option(device, s_bridge, option, FALSE);
+
+    if (!bridge_set_vlan_options(device, (NMSettingBridge *) s_bridge)) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+_bt_register_bridge_cb(GError *error, gpointer user_data)
+{
+    NMDeviceBridge *self;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+
+    g_clear_object(&self->bt_cancellable);
+
+    if (error) {
+        _LOGD(LOGD_DEVICE, "bluetooth NAP server failed to register bridge: %s", error->message);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_BT_FAILED);
+        return;
+    }
+
+    nm_device_activate_schedule_stage2_device_config(NM_DEVICE(self), FALSE);
+}
+
+void
+_nm_device_bridge_notify_unregister_bt_nap(NMDevice *device, const char *reason)
+{
+    NMDeviceBridge *self = NM_DEVICE_BRIDGE(device);
+
+    _LOGD(LOGD_DEVICE,
+          "bluetooth NAP server unregistered from bridge: %s%s",
+          reason,
+          self->bt_registered ? "" : " (was no longer registered)");
+
+    nm_clear_g_cancellable(&self->bt_cancellable);
+
+    if (self->bt_registered) {
+        self->bt_registered = FALSE;
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_BT_FAILED);
+    }
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceBridge *    self = NM_DEVICE_BRIDGE(device);
+    NMConnection *      connection;
+    NMSettingBluetooth *s_bt;
+    gs_free_error GError *error = NULL;
+
+    connection = nm_device_get_applied_connection(device);
+
+    s_bt = _nm_connection_get_setting_bluetooth_for_nap(connection);
+    if (!s_bt)
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    if (!nm_bt_vtable_network_server) {
+        _LOGD(LOGD_DEVICE, "bluetooth NAP server failed because bluetooth plugin not available");
+        *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (self->bt_cancellable)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    if (self->bt_registered)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    self->bt_cancellable = g_cancellable_new();
+    if (!nm_bt_vtable_network_server->register_bridge(nm_bt_vtable_network_server,
+                                                      nm_setting_bluetooth_get_bdaddr(s_bt),
+                                                      device,
+                                                      self->bt_cancellable,
+                                                      _bt_register_bridge_cb,
+                                                      device,
+                                                      &error)) {
+        _LOGD(LOGD_DEVICE, "bluetooth NAP server failed to register bridge: %s", error->message);
+        *out_failure_reason = NM_DEVICE_STATE_REASON_BT_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    self->bt_registered = TRUE;
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceBridge *self = NM_DEVICE_BRIDGE(device);
+
+    _LOGD(LOGD_DEVICE,
+          "deactivate bridge%s",
+          self->bt_registered ? " (registered as NAP bluetooth device)" : "");
+
+    self->vlan_configured = FALSE;
+
+    nm_clear_g_cancellable(&self->bt_cancellable);
+
+    if (self->bt_registered) {
+        self->bt_registered = FALSE;
+        nm_bt_vtable_network_server->unregister_bridge(nm_bt_vtable_network_server, device);
+    }
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    NMDeviceBridge *     self = NM_DEVICE_BRIDGE(device);
+    NMConnection *       master_connection;
+    NMSettingBridge *    s_bridge;
+    NMSettingBridgePort *s_port;
+
+    if (configure) {
+        if (!nm_platform_link_enslave(nm_device_get_platform(device),
+                                      nm_device_get_ip_ifindex(device),
+                                      nm_device_get_ip_ifindex(slave)))
+            return FALSE;
+
+        master_connection = nm_device_get_applied_connection(device);
+        nm_assert(master_connection);
+        s_bridge = nm_connection_get_setting_bridge(master_connection);
+        nm_assert(s_bridge);
+        s_port = nm_connection_get_setting_bridge_port(connection);
+
+        bridge_set_vlan_options(device, s_bridge);
+
+        if (nm_setting_bridge_get_vlan_filtering(s_bridge)) {
+            gs_free const NMPlatformBridgeVlan **plat_vlans = NULL;
+            gs_unref_ptrarray GPtrArray *vlans              = NULL;
+
+            if (s_port)
+                g_object_get(s_port, NM_SETTING_BRIDGE_PORT_VLANS, &vlans, NULL);
+
+            plat_vlans = setting_vlans_to_platform(vlans);
+
+            /* Since the link was just enslaved, there are no existing VLANs
+             * (except for the default one) and so there's no need to flush. */
+
+            if (plat_vlans
+                && !nm_platform_link_set_bridge_vlans(nm_device_get_platform(slave),
+                                                      nm_device_get_ifindex(slave),
+                                                      TRUE,
+                                                      plat_vlans))
+                return FALSE;
+        }
+
+        commit_slave_options(slave, s_port);
+
+        _LOGI(LOGD_BRIDGE, "attached bridge port %s", nm_device_get_ip_iface(slave));
+    } else {
+        _LOGI(LOGD_BRIDGE, "bridge port %s was attached", nm_device_get_ip_iface(slave));
+    }
+
+    return TRUE;
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{
+    NMDeviceBridge *self = NM_DEVICE_BRIDGE(device);
+    gboolean        success;
+    int             ifindex_slave;
+    int             ifindex;
+
+    if (configure) {
+        ifindex = nm_device_get_ifindex(device);
+        if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex))
+            configure = FALSE;
+    }
+
+    ifindex_slave = nm_device_get_ip_ifindex(slave);
+
+    if (ifindex_slave <= 0) {
+        _LOGD(LOGD_TEAM, "bond slave %s is already released", nm_device_get_ip_iface(slave));
+        return;
+    }
+
+    if (configure) {
+        success = nm_platform_link_release(nm_device_get_platform(device),
+                                           nm_device_get_ip_ifindex(device),
+                                           ifindex_slave);
+
+        if (success) {
+            _LOGI(LOGD_BRIDGE, "detached bridge port %s", nm_device_get_ip_iface(slave));
+        } else {
+            _LOGW(LOGD_BRIDGE, "failed to detach bridge port %s", nm_device_get_ip_iface(slave));
+        }
+    } else {
+        _LOGI(LOGD_BRIDGE, "bridge port %s was detached", nm_device_get_ip_iface(slave));
+    }
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    NMSettingWired *    s_wired;
+    NMSettingBridge *   s_bridge;
+    const char *        iface = nm_device_get_iface(device);
+    const char *        hwaddr;
+    gs_free char *      hwaddr_cloned = NULL;
+    guint8              mac_address[NM_UTILS_HWADDR_LEN_MAX];
+    NMPlatformLnkBridge props;
+    int                 r;
+    guint32             mtu = 0;
+
+    nm_assert(iface);
+
+    s_bridge = nm_connection_get_setting_bridge(connection);
+    nm_assert(s_bridge);
+
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (s_wired)
+        mtu = nm_setting_wired_get_mtu(s_wired);
+
+    hwaddr = nm_setting_bridge_get_mac_address(s_bridge);
+    if (!hwaddr
+        && nm_device_hw_addr_get_cloned(device, connection, FALSE, &hwaddr_cloned, NULL, NULL)) {
+        /* FIXME: we set the MAC address when creating the interface, while the
+         * NMDevice is still unrealized. As we afterwards realize the device, it
+         * forgets the parameters for the cloned MAC address, and in stage 1
+         * it might create a different MAC address. That should be fixed by
+         * better handling device realization. */
+        hwaddr = hwaddr_cloned;
+    }
+
+    if (hwaddr) {
+        if (!nm_utils_hwaddr_aton(hwaddr, mac_address, ETH_ALEN)) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "Invalid hardware address '%s'",
+                        hwaddr);
+            g_return_val_if_reached(FALSE);
+        }
+    }
+
+    props = (NMPlatformLnkBridge){
+        .forward_delay = _DEFAULT_IF_ZERO(nm_setting_bridge_get_forward_delay(s_bridge) * 100u,
+                                          NM_BRIDGE_FORWARD_DELAY_DEF_SYS),
+        .hello_time    = _DEFAULT_IF_ZERO(nm_setting_bridge_get_hello_time(s_bridge) * 100u,
+                                       NM_BRIDGE_HELLO_TIME_DEF_SYS),
+        .max_age       = _DEFAULT_IF_ZERO(nm_setting_bridge_get_max_age(s_bridge) * 100u,
+                                    NM_BRIDGE_MAX_AGE_DEF_SYS),
+        .ageing_time   = _DEFAULT_IF_ZERO(nm_setting_bridge_get_ageing_time(s_bridge) * 100u,
+                                        NM_BRIDGE_AGEING_TIME_DEF_SYS),
+        .stp_state     = nm_setting_bridge_get_stp(s_bridge),
+        .priority      = nm_setting_bridge_get_priority(s_bridge),
+        .vlan_protocol = to_sysfs_vlan_protocol_sys(nm_setting_bridge_get_vlan_protocol(s_bridge)),
+        .vlan_stats_enabled = nm_setting_bridge_get_vlan_stats_enabled(s_bridge),
+        .group_fwd_mask     = nm_setting_bridge_get_group_forward_mask(s_bridge),
+        .mcast_snooping     = nm_setting_bridge_get_multicast_snooping(s_bridge),
+        .mcast_router =
+            to_sysfs_multicast_router_sys(nm_setting_bridge_get_multicast_router(s_bridge)),
+        .mcast_query_use_ifaddr    = nm_setting_bridge_get_multicast_query_use_ifaddr(s_bridge),
+        .mcast_querier             = nm_setting_bridge_get_multicast_querier(s_bridge),
+        .mcast_hash_max            = nm_setting_bridge_get_multicast_hash_max(s_bridge),
+        .mcast_last_member_count   = nm_setting_bridge_get_multicast_last_member_count(s_bridge),
+        .mcast_startup_query_count = nm_setting_bridge_get_multicast_startup_query_count(s_bridge),
+        .mcast_last_member_interval =
+            nm_setting_bridge_get_multicast_last_member_interval(s_bridge),
+        .mcast_membership_interval = nm_setting_bridge_get_multicast_membership_interval(s_bridge),
+        .mcast_querier_interval    = nm_setting_bridge_get_multicast_querier_interval(s_bridge),
+        .mcast_query_interval      = nm_setting_bridge_get_multicast_query_interval(s_bridge),
+        .mcast_query_response_interval =
+            nm_setting_bridge_get_multicast_query_response_interval(s_bridge),
+        .mcast_startup_query_interval =
+            nm_setting_bridge_get_multicast_startup_query_interval(s_bridge),
+    };
+
+    to_sysfs_group_address_sys(nm_setting_bridge_get_group_address(s_bridge), &props.group_addr);
+
+    /* If mtu != 0, we set the MTU of the new bridge at creation time. However, kernel will still
+     * automatically adjust the MTU of the bridge based on the minimum of the slave's MTU.
+     * We don't want this automatism as the user asked for a fixed MTU.
+     *
+     * To workaround this behavior of kernel, we will later toggle the MTU twice. See
+     * NMDeviceClass.mtu_force_set. */
+    r = nm_platform_link_bridge_add(nm_device_get_platform(device),
+                                    iface,
+                                    hwaddr ? mac_address : NULL,
+                                    hwaddr ? ETH_ALEN : 0,
+                                    mtu,
+                                    &props,
+                                    out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create bridge interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_bridge_init(NMDeviceBridge *self)
+{
+    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_bridge = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_BRIDGE,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves",
+                                                             "ao",
+                                                             NM_DEVICE_SLAVES), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_bridge_class_init(NMDeviceBridgeClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_bridge);
+
+    device_class->connection_type_supported = NM_SETTING_BRIDGE_SETTING_NAME;
+    device_class->link_types                = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_BRIDGE);
+
+    device_class->is_master                   = TRUE;
+    device_class->mtu_force_set               = TRUE;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+
+    device_class->update_connection              = update_connection;
+    device_class->master_update_slave_connection = master_update_slave_connection;
+
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->act_stage1_prepare                     = act_stage1_prepare;
+    device_class->act_stage2_config                      = act_stage2_config;
+    device_class->deactivate                             = deactivate;
+    device_class->enslave_slave                          = enslave_slave;
+    device_class->release_slave                          = release_slave;
+    device_class->get_configured_mtu                     = nm_device_get_configured_mtu_for_wired;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_BRIDGE_DEVICE_FACTORY (nm_bridge_device_factory_get_type())
+#define NM_BRIDGE_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BRIDGE_DEVICE_FACTORY, NMBridgeDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_BRIDGE,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_DRIVER,
+                        "bridge",
+                        NM_DEVICE_TYPE_DESC,
+                        "Bridge",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_BRIDGE,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_BRIDGE,
+                        NULL);
+}
+
+static gboolean
+match_connection(NMDeviceFactory *factory, NMConnection *connection)
+{
+    const char *type = nm_connection_get_connection_type(connection);
+
+    if (nm_streq(type, NM_SETTING_BRIDGE_SETTING_NAME))
+        return TRUE;
+
+    nm_assert(nm_streq(type, NM_SETTING_BLUETOOTH_SETTING_NAME));
+
+    if (!_nm_connection_get_setting_bluetooth_for_nap(connection))
+        return FALSE;
+
+    if (!g_type_from_name("NMBluezManager")) {
+        /* bluetooth NAP connections are handled by bridge factory. However,
+         * it needs help from the bluetooth plugin, so if the plugin is not loaded,
+         * we claim not to support it. */
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    BRIDGE,
+    Bridge,
+    bridge,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_BRIDGE)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_BRIDGE_SETTING_NAME,
+                                                NM_SETTING_BLUETOOTH_SETTING_NAME),
+    factory_class->create_device    = create_device;
+    factory_class->match_connection = match_connection;);
diff --git a/src/core/devices/nm-device-bridge.h b/src/core/devices/nm-device-bridge.h
new file mode 100644
index 00000000..32437947
--- /dev/null
+++ b/src/core/devices/nm-device-bridge.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_BRIDGE_H__
+#define __NETWORKMANAGER_DEVICE_BRIDGE_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_BRIDGE (nm_device_bridge_get_type())
+#define NM_DEVICE_BRIDGE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridge))
+#define NM_DEVICE_BRIDGE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridgeClass))
+#define NM_IS_DEVICE_BRIDGE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_BRIDGE))
+#define NM_IS_DEVICE_BRIDGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_BRIDGE))
+#define NM_DEVICE_BRIDGE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_BRIDGE, NMDeviceBridgeClass))
+
+typedef struct _NMDeviceBridge      NMDeviceBridge;
+typedef struct _NMDeviceBridgeClass NMDeviceBridgeClass;
+
+GType nm_device_bridge_get_type(void);
+
+extern const NMBtVTableNetworkServer *nm_bt_vtable_network_server;
+
+void _nm_device_bridge_notify_unregister_bt_nap(NMDevice *device, const char *reason);
+
+#endif /* __NETWORKMANAGER_DEVICE_BRIDGE_H__ */
diff --git a/src/core/devices/nm-device-dummy.c b/src/core/devices/nm-device-dummy.c
new file mode 100644
index 00000000..13cfd3b0
--- /dev/null
+++ b/src/core/devices/nm-device-dummy.c
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-dummy.h"
+
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-dummy.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceDummy
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceDummy {
+    NMDevice parent;
+};
+
+struct _NMDeviceDummyClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceDummy, nm_device_dummy, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingDummy *s_dummy;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_DUMMY_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Dummy connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_dummy = nm_connection_get_setting_dummy(connection);
+    if (!s_dummy) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'dummy' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMSettingDummy *s_dummy = nm_connection_get_setting_dummy(connection);
+
+    if (!s_dummy) {
+        s_dummy = (NMSettingDummy *) nm_setting_dummy_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_dummy);
+    }
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *    iface = nm_device_get_iface(device);
+    NMSettingDummy *s_dummy;
+    int             r;
+
+    s_dummy = nm_connection_get_setting_dummy(connection);
+    g_assert(s_dummy);
+
+    r = nm_platform_link_dummy_add(nm_device_get_platform(device), iface, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create dummy interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_dummy_init(NMDeviceDummy *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_dummy = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_DUMMY,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_dummy_class_init(NMDeviceDummyClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_dummy);
+
+    device_class->connection_type_supported        = NM_SETTING_DUMMY_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_DUMMY_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_DUMMY);
+
+    device_class->complete_connection                    = complete_connection;
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->update_connection                      = update_connection;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->get_configured_mtu                     = nm_device_get_configured_mtu_for_wired;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_DUMMY_DEVICE_FACTORY (nm_dummy_device_factory_get_type())
+#define NM_DUMMY_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DUMMY_DEVICE_FACTORY, NMDummyDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_DUMMY,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Dummy",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_DUMMY,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_DUMMY,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    DUMMY,
+    Dummy,
+    dummy,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_DUMMY)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_DUMMY_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-dummy.h b/src/core/devices/nm-device-dummy.h
new file mode 100644
index 00000000..2845b4eb
--- /dev/null
+++ b/src/core/devices/nm-device-dummy.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_DUMMY_H__
+#define __NETWORKMANAGER_DEVICE_DUMMY_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_DUMMY (nm_device_dummy_get_type())
+#define NM_DEVICE_DUMMY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummy))
+#define NM_DEVICE_DUMMY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_DUMMY, NMDeviceDummyClass))
+#define NM_IS_DEVICE_DUMMY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_DUMMY))
+#define NM_IS_DEVICE_DUMMY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_DUMMY))
+#define NM_DEVICE_DUMMY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_DUMMY, NMDeviceDummyClass))
+
+typedef struct _NMDeviceDummy      NMDeviceDummy;
+typedef struct _NMDeviceDummyClass NMDeviceDummyClass;
+
+GType nm_device_dummy_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_DUMMY_H__ */
diff --git a/src/core/devices/nm-device-ethernet-utils.c b/src/core/devices/nm-device-ethernet-utils.c
new file mode 100644
index 00000000..a36d12b7
--- /dev/null
+++ b/src/core/devices/nm-device-ethernet-utils.c
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ethernet-utils.h"
+
+#include "settings/nm-settings-connection.h"
+
+char *
+nm_device_ethernet_utils_get_default_wired_name(GHashTable *existing_ids)
+{
+    char *temp;
+    int   i;
+
+    /* Find the next available unique connection name */
+    for (i = 1; i < G_MAXINT; i++) {
+        temp = g_strdup_printf(_("Wired connection %d"), i);
+        if (!existing_ids || !g_hash_table_contains(existing_ids, temp))
+            return temp;
+        g_free(temp);
+    }
+    return NULL;
+}
diff --git a/src/core/devices/nm-device-ethernet-utils.h b/src/core/devices/nm-device-ethernet-utils.h
new file mode 100644
index 00000000..133340fc
--- /dev/null
+++ b/src/core/devices/nm-device-ethernet-utils.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H__
+#define __NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H__
+
+char *nm_device_ethernet_utils_get_default_wired_name(GHashTable *existing_ids);
+
+#endif /* NETWORKMANAGER_DEVICE_ETHERNET_UTILS_H */
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
new file mode 100644
index 00000000..44428869
--- /dev/null
+++ b/src/core/devices/nm-device-ethernet.c
@@ -0,0 +1,2128 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2014 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ethernet.h"
+
+#include <netinet/in.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <libudev.h>
+#include <linux/if_ether.h>
+
+#include "nm-device-private.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "NetworkManagerUtils.h"
+#include "supplicant/nm-supplicant-manager.h"
+#include "supplicant/nm-supplicant-interface.h"
+#include "supplicant/nm-supplicant-config.h"
+#include "ppp/nm-ppp-manager.h"
+#include "ppp/nm-ppp-manager-call.h"
+#include "ppp/nm-ppp-status.h"
+#include "platform/nm-platform.h"
+#include "nm-platform/nm-platform-utils.h"
+#include "nm-dcb.h"
+#include "settings/nm-settings-connection.h"
+#include "nm-config.h"
+#include "nm-device-ethernet-utils.h"
+#include "settings/nm-settings.h"
+#include "nm-device-factory.h"
+#include "nm-core-internal.h"
+#include "NetworkManagerUtils.h"
+#include "nm-udev-aux/nm-udev-utils.h"
+#include "nm-device-veth.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceEthernet
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+#define PPPOE_RECONNECT_DELAY 7
+#define PPPOE_ENCAP_OVERHEAD  8 /* 2 bytes for PPP, 6 for PPPoE */
+
+#define SUPPLICANT_LNK_TIMEOUT_SEC 15
+
+/*****************************************************************************/
+
+typedef enum {
+    DCB_WAIT_UNKNOWN = 0,
+    /* Ensure carrier is up before enabling DCB */
+    DCB_WAIT_CARRIER_PREENABLE_UP,
+    /* Wait for carrier down when device starts enabling */
+    DCB_WAIT_CARRIER_PRECONFIG_DOWN,
+    /* Wait for carrier up when device has finished enabling */
+    DCB_WAIT_CARRIER_PRECONFIG_UP,
+    /* Wait carrier down when device starts configuring */
+    DCB_WAIT_CARRIER_POSTCONFIG_DOWN,
+    /* Wait carrier up when device has finished configuring */
+    DCB_WAIT_CARRIER_POSTCONFIG_UP,
+} DcbWait;
+
+typedef struct _NMDeviceEthernetPrivate {
+    /* s390 */
+    char *      subchan1;
+    char *      subchan2;
+    char *      subchan3;
+    char *      subchannels;      /* Composite used for checking unmanaged specs */
+    char **     subchannels_dbus; /* Array exported on D-Bus */
+    char *      s390_nettype;
+    GHashTable *s390_options;
+
+    guint32 speed;
+    gulong  carrier_id;
+
+    struct {
+        NMSupplicantManager *        mgr;
+        NMSupplMgrCreateIfaceHandle *create_handle;
+        NMSupplicantInterface *      iface;
+
+        gulong iface_state_id;
+        gulong auth_state_id;
+
+        guint con_timeout_id;
+
+        guint lnk_timeout_id;
+
+        bool is_associated : 1;
+    } supplicant;
+
+    NMActRequestGetSecretsCallId *wired_secrets_id;
+
+    /* PPPoE */
+    NMPPPManager *ppp_manager;
+    gint32        last_pppoe_time;
+    guint         pppoe_wait_id;
+
+    /* DCB */
+    DcbWait dcb_wait;
+    guint   dcb_timeout_id;
+
+    guint32 ethtool_prev_speed;
+
+    NMPlatformLinkDuplexType ethtool_prev_duplex : 3;
+
+    bool dcb_handle_carrier_changes : 1;
+
+    bool ethtool_prev_set : 1;
+    bool ethtool_prev_autoneg : 1;
+
+} NMDeviceEthernetPrivate;
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceEthernet, PROP_SPEED, PROP_S390_SUBCHANNELS, );
+
+/*****************************************************************************/
+
+G_DEFINE_TYPE(NMDeviceEthernet, nm_device_ethernet, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_ETHERNET_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE_PTR(self, NMDeviceEthernet, NM_IS_DEVICE_ETHERNET, NMDevice)
+
+/*****************************************************************************/
+
+static void wired_secrets_cancel(NMDeviceEthernet *self);
+
+/*****************************************************************************/
+
+static char *
+get_link_basename(const char *parent_path, const char *name, GError **error)
+{
+    char *link_dest, *path;
+    char *result = NULL;
+
+    path      = g_strdup_printf("%s/%s", parent_path, name);
+    link_dest = g_file_read_link(path, error);
+    if (link_dest) {
+        result = g_path_get_basename(link_dest);
+        g_free(link_dest);
+    }
+    g_free(path);
+    return result;
+}
+
+static void
+_update_s390_subchannels(NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    struct udev_device *     dev    = NULL;
+    struct udev_device *     parent = NULL;
+    const char *             parent_path, *item;
+    int                      ifindex;
+    GDir *                   dir;
+    GError *                 error = NULL;
+
+    if (priv->subchannels) {
+        /* only read the subchannels once. For one, we don't expect them to change
+         * on multiple invocations. Second, we didn't implement proper reloading.
+         * Proper reloading might also be complicated, because the subchannels are
+         * used to match on devices based on a device-spec. Thus, it's not clear
+         * what it means to change afterwards. */
+        return;
+    }
+
+    ifindex = nm_device_get_ifindex((NMDevice *) self);
+    dev     = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)), ifindex);
+    if (!dev)
+        return;
+
+    /* Try for the "ccwgroup" parent */
+    parent = udev_device_get_parent_with_subsystem_devtype(dev, "ccwgroup", NULL);
+    if (!parent) {
+        /* FIXME: whatever 'lcs' devices' subsystem is here... */
+
+        /* Not an s390 device */
+        return;
+    }
+
+    parent_path = udev_device_get_syspath(parent);
+    dir         = g_dir_open(parent_path, 0, &error);
+    if (!dir) {
+        _LOGW(LOGD_DEVICE | LOGD_PLATFORM,
+              "update-s390: failed to open directory '%s': %s",
+              parent_path,
+              error->message);
+        g_clear_error(&error);
+        return;
+    }
+
+    while ((item = g_dir_read_name(dir))) {
+        if (!strcmp(item, "cdev0")) {
+            priv->subchan1 = get_link_basename(parent_path, "cdev0", &error);
+        } else if (!strcmp(item, "cdev1")) {
+            priv->subchan2 = get_link_basename(parent_path, "cdev1", &error);
+        } else if (!strcmp(item, "cdev2")) {
+            priv->subchan3 = get_link_basename(parent_path, "cdev2", &error);
+        } else if (!strcmp(item, "driver")) {
+            priv->s390_nettype = get_link_basename(parent_path, "driver", &error);
+        } else if (!strcmp(item, "layer2") || !strcmp(item, "portname")
+                   || !strcmp(item, "portno")) {
+            gs_free char *path = NULL, *value = NULL;
+
+            path  = g_strdup_printf("%s/%s", parent_path, item);
+            value = nm_platform_sysctl_get(nm_device_get_platform(NM_DEVICE(self)),
+                                           NMP_SYSCTL_PATHID_ABSOLUTE(path));
+
+            if (!strcmp(item, "portname") && !g_strcmp0(value, "no portname required")) {
+                /* Do nothing */
+            } else if (value && *value) {
+                g_hash_table_insert(priv->s390_options, g_strdup(item), value);
+                value = NULL;
+            } else
+                _LOGW(LOGD_DEVICE | LOGD_PLATFORM, "update-s390: error reading %s", path);
+        }
+
+        if (error) {
+            _LOGW(LOGD_DEVICE | LOGD_PLATFORM,
+                  "update-s390: failed reading sysfs for %s (%s)",
+                  item,
+                  error->message);
+            g_clear_error(&error);
+        }
+    }
+
+    g_dir_close(dir);
+
+    if (priv->subchan3) {
+        priv->subchannels =
+            g_strdup_printf("%s,%s,%s", priv->subchan1, priv->subchan2, priv->subchan3);
+    } else if (priv->subchan2) {
+        priv->subchannels = g_strdup_printf("%s,%s", priv->subchan1, priv->subchan2);
+    } else
+        priv->subchannels = g_strdup(priv->subchan1);
+
+    priv->subchannels_dbus    = g_new(char *, 3 + 1);
+    priv->subchannels_dbus[0] = g_strdup(priv->subchan1);
+    priv->subchannels_dbus[1] = g_strdup(priv->subchan2);
+    priv->subchannels_dbus[2] = g_strdup(priv->subchan3);
+    priv->subchannels_dbus[3] = NULL;
+
+    _LOGI(LOGD_DEVICE | LOGD_PLATFORM,
+          "update-s390: found s390 '%s' subchannels [%s]",
+          nm_device_get_driver((NMDevice *) self) ?: "(unknown driver)",
+          priv->subchannels);
+
+    _notify(self, PROP_S390_SUBCHANNELS);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    if (new_state > NM_DEVICE_STATE_ACTIVATED)
+        wired_secrets_cancel(NM_DEVICE_ETHERNET(device));
+}
+
+static void
+nm_device_ethernet_init(NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetPrivate);
+    self->_priv = priv;
+
+    priv->s390_options = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    NMDeviceEthernet *self    = NM_DEVICE_ETHERNET(device);
+    int               ifindex = nm_device_get_ifindex(device);
+
+    if (ifindex > 0) {
+        if (nm_platform_link_supports_carrier_detect(nm_device_get_platform(device), ifindex))
+            return NM_DEVICE_CAP_CARRIER_DETECT;
+        else {
+            _LOGI(LOGD_PLATFORM,
+                  "driver '%s' does not support carrier detection.",
+                  nm_device_get_driver(device));
+        }
+    }
+
+    return NM_DEVICE_CAP_NONE;
+}
+
+static guint32
+_subchannels_count_num(const char *const *array)
+{
+    int i;
+
+    if (!array)
+        return 0;
+    for (i = 0; array[i]; i++)
+        /* NOP */;
+    return i;
+}
+
+static gboolean
+match_subchans(NMDeviceEthernet *self, NMSettingWired *s_wired, gboolean *try_mac)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    const char *const *      subchans;
+    guint32                  num1, num2;
+    int                      i;
+
+    *try_mac = TRUE;
+
+    subchans = nm_setting_wired_get_s390_subchannels(s_wired);
+    num1     = _subchannels_count_num(subchans);
+    num2     = _subchannels_count_num((const char *const *) priv->subchannels_dbus);
+    /* connection has no subchannels */
+    if (num1 == 0)
+        return TRUE;
+    /* connection requires subchannels but the device has none */
+    if (num2 == 0)
+        return FALSE;
+    /* number of subchannels differ */
+    if (num1 != num2)
+        return FALSE;
+
+    /* Make sure each subchannel in the connection is a subchannel of this device */
+    for (i = 0; subchans[i]; i++) {
+        const char *candidate = subchans[i];
+
+        if ((priv->subchan1 && !strcmp(priv->subchan1, candidate))
+            || (priv->subchan2 && !strcmp(priv->subchan2, candidate))
+            || (priv->subchan3 && !strcmp(priv->subchan3, candidate)))
+            continue;
+
+        return FALSE; /* a subchannel was not found */
+    }
+
+    *try_mac = FALSE;
+    return TRUE;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceEthernet *self = NM_DEVICE_ETHERNET(device);
+    NMSettingWired *  s_wired;
+
+    if (!NM_DEVICE_CLASS(nm_device_ethernet_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME)
+        || (nm_connection_is_type(connection, NM_SETTING_VETH_SETTING_NAME)
+            && NM_IS_DEVICE_VETH(device))) {
+        s_wired = nm_connection_get_setting_wired(connection);
+    } else {
+        s_wired =
+            _nm_connection_check_main_setting(connection, NM_SETTING_WIRED_SETTING_NAME, error);
+        if (!s_wired)
+            return FALSE;
+    }
+
+    if (s_wired) {
+        const char *       mac, *perm_hw_addr;
+        gboolean           try_mac = TRUE;
+        const char *const *mac_blacklist;
+        int                i;
+
+        if (!match_subchans(self, s_wired, &try_mac)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "s390 subchannels don't match");
+            return FALSE;
+        }
+
+        perm_hw_addr = nm_device_get_permanent_hw_address(device);
+        mac          = nm_setting_wired_get_mac_address(s_wired);
+        if (perm_hw_addr) {
+            if (try_mac && mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "permanent MAC address doesn't match");
+                return FALSE;
+            }
+
+            /* Check for MAC address blacklist */
+            mac_blacklist = nm_setting_wired_get_mac_address_blacklist(s_wired);
+            for (i = 0; mac_blacklist[i]; i++) {
+                if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) {
+                    nm_utils_error_set_literal(error,
+                                               NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                               "invalid MAC in blacklist");
+                    return FALSE;
+                }
+
+                if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) {
+                    nm_utils_error_set_literal(error,
+                                               NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                               "permanent MAC address of device blacklisted");
+                    return FALSE;
+                }
+            }
+        } else if (mac) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "device has no permanent MAC address to match");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* 802.1X */
+
+static void
+supplicant_interface_release(NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->supplicant.create_handle,
+                     nm_supplicant_manager_create_interface_cancel);
+
+    nm_clear_g_source(&priv->supplicant.lnk_timeout_id);
+    nm_clear_g_source(&priv->supplicant.con_timeout_id);
+    nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id);
+    nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.auth_state_id);
+
+    if (priv->supplicant.iface) {
+        nm_supplicant_interface_disconnect(priv->supplicant.iface);
+        g_clear_object(&priv->supplicant.iface);
+    }
+}
+
+static void
+supplicant_auth_state_changed(NMSupplicantInterface *iface,
+                              GParamSpec *           pspec,
+                              NMDeviceEthernet *     self)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSupplicantAuthState    state;
+
+    state = nm_supplicant_interface_get_auth_state(priv->supplicant.iface);
+    _LOGD(LOGD_CORE, "supplicant auth state changed to %u", (unsigned) state);
+
+    if (state == NM_SUPPLICANT_AUTH_STATE_SUCCESS) {
+        nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id);
+        nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+    }
+}
+
+static gboolean
+wired_auth_is_optional(NMDeviceEthernet *self)
+{
+    NMSetting8021x *s_8021x;
+
+    s_8021x = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_802_1X);
+    g_return_val_if_fail(s_8021x, FALSE);
+    return nm_setting_802_1x_get_optional(s_8021x);
+}
+
+static void
+wired_auth_cond_fail(NMDeviceEthernet *self, NMDeviceStateReason reason)
+{
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMDevice *               device = NM_DEVICE(self);
+
+    if (wired_auth_is_optional(self)) {
+        _LOGI(
+            LOGD_DEVICE | LOGD_ETHER,
+            "Activation: (ethernet) 802.1X authentication is optional, continuing after a failure");
+        if (NM_IN_SET(nm_device_get_state(device),
+                      NM_DEVICE_STATE_CONFIG,
+                      NM_DEVICE_STATE_NEED_AUTH))
+            nm_device_activate_schedule_stage3_ip_config_start(device);
+
+        if (!priv->supplicant.auth_state_id) {
+            priv->supplicant.auth_state_id =
+                g_signal_connect(priv->supplicant.iface,
+                                 "notify::" NM_SUPPLICANT_INTERFACE_AUTH_STATE,
+                                 G_CALLBACK(supplicant_auth_state_changed),
+                                 self);
+        }
+        return;
+    }
+
+    supplicant_interface_release(self);
+    nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason);
+}
+
+static void
+wired_secrets_cb(NMActRequest *                req,
+                 NMActRequestGetSecretsCallId *call_id,
+                 NMSettingsConnection *        connection,
+                 GError *                      error,
+                 gpointer                      user_data)
+{
+    NMDeviceEthernet *       self   = user_data;
+    NMDevice *               device = user_data;
+    NMDeviceEthernetPrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE_ETHERNET(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->wired_secrets_id == call_id);
+
+    priv->wired_secrets_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == connection);
+
+    if (error) {
+        _LOGW(LOGD_ETHER, "%s", error->message);
+        wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    supplicant_interface_release(self);
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+wired_secrets_cancel(NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    if (priv->wired_secrets_id)
+        nm_act_request_cancel_secrets(NULL, priv->wired_secrets_id);
+    nm_assert(!priv->wired_secrets_id);
+}
+
+static void
+wired_secrets_get_secrets(NMDeviceEthernet *           self,
+                          const char *                 setting_name,
+                          NMSecretAgentGetSecretsFlags flags)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMActRequest *           req;
+
+    wired_secrets_cancel(self);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv->wired_secrets_id =
+        nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, wired_secrets_cb, self);
+    g_return_if_fail(priv->wired_secrets_id);
+}
+
+static gboolean
+supplicant_lnk_timeout_cb(gpointer user_data)
+{
+    NMDeviceEthernet *       self   = NM_DEVICE_ETHERNET(user_data);
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMDevice *               device = NM_DEVICE(self);
+    NMActRequest *           req;
+    NMConnection *           applied_connection;
+    const char *             setting_name;
+
+    priv->supplicant.lnk_timeout_id = 0;
+
+    req = nm_device_get_act_request(device);
+
+    if (nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) {
+        wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
+        return G_SOURCE_REMOVE;
+    }
+
+    /* Disconnect event during initial authentication and credentials
+     * ARE checked - we are likely to have wrong key.  Ask the user for
+     * another one.
+     */
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG)
+        goto time_out;
+
+    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    setting_name       = nm_connection_need_secrets(applied_connection, NULL);
+    if (!setting_name)
+        goto time_out;
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER,
+          "Activation: (ethernet) disconnected during authentication, asking for new key.");
+    if (!wired_auth_is_optional(self))
+        supplicant_interface_release(self);
+
+    nm_device_state_changed(device,
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+    wired_secrets_get_secrets(self, setting_name, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
+
+    return G_SOURCE_REMOVE;
+
+time_out:
+    _LOGW(LOGD_DEVICE | LOGD_ETHER, "link timed out.");
+    wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+
+    return G_SOURCE_REMOVE;
+}
+
+static NMSupplicantConfig *
+build_supplicant_config(NMDeviceEthernet *self, GError **error)
+{
+    const char *        con_uuid;
+    NMSupplicantConfig *config = NULL;
+    NMSetting8021x *    security;
+    NMConnection *      connection;
+    guint32             mtu;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    g_return_val_if_fail(connection, NULL);
+
+    con_uuid = nm_connection_get_uuid(connection);
+    mtu      = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
+                                   nm_device_get_ifindex(NM_DEVICE(self)));
+
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+
+    security = nm_connection_get_setting_802_1x(connection);
+    if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) {
+        g_prefix_error(error, "802-1x-setting: ");
+        g_clear_object(&config);
+    }
+
+    return config;
+}
+
+static void
+supplicant_iface_state_is_completed(NMDeviceEthernet *self, NMSupplicantInterfaceState state)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    if (state == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) {
+        nm_clear_g_source(&priv->supplicant.lnk_timeout_id);
+        nm_clear_g_source(&priv->supplicant.con_timeout_id);
+
+        /* If this is the initial association during device activation,
+         * schedule the next activation stage.
+         */
+        if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) {
+            _LOGI(LOGD_DEVICE | LOGD_ETHER,
+                  "Activation: (ethernet) Stage 2 of 5 (Device Configure) successful.");
+            nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self));
+        }
+        return;
+    }
+
+    if (!priv->supplicant.lnk_timeout_id && !priv->supplicant.con_timeout_id)
+        priv->supplicant.lnk_timeout_id =
+            g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self);
+}
+
+static void
+supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data)
+{
+    NMDeviceEthernet *       self;
+    NMDeviceEthernetPrivate *priv;
+
+    if (nm_utils_error_is_cancelled_or_disposing(error))
+        return;
+
+    self = NM_DEVICE_ETHERNET(user_data);
+    priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    if (error) {
+        supplicant_interface_release(self);
+        nm_device_queue_state(NM_DEVICE(self),
+                              NM_DEVICE_STATE_FAILED,
+                              NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED);
+        return;
+    }
+
+    nm_assert(!priv->supplicant.lnk_timeout_id);
+    nm_assert(!priv->supplicant.is_associated);
+
+    priv->supplicant.is_associated = TRUE;
+    supplicant_iface_state_is_completed(self,
+                                        nm_supplicant_interface_get_state(priv->supplicant.iface));
+}
+
+static gboolean
+supplicant_iface_start(NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv              = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    gs_unref_object NMSupplicantConfig *config = NULL;
+    gs_free_error GError *error                = NULL;
+
+    config = build_supplicant_config(self, &error);
+    if (!config) {
+        _LOGE(LOGD_DEVICE | LOGD_ETHER,
+              "Activation: (ethernet) couldn't build security configuration: %s",
+              error->message);
+        supplicant_interface_release(self);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED);
+        return FALSE;
+    }
+
+    nm_supplicant_interface_disconnect(priv->supplicant.iface);
+    nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self);
+    return TRUE;
+}
+
+static void
+supplicant_iface_state_cb(NMSupplicantInterface *iface,
+                          int                    new_state_i,
+                          int                    old_state_i,
+                          int                    disconnect_reason,
+                          gpointer               user_data)
+{
+    NMDeviceEthernet *         self      = NM_DEVICE_ETHERNET(user_data);
+    NMDeviceEthernetPrivate *  priv      = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSupplicantInterfaceState new_state = new_state_i;
+    NMSupplicantInterfaceState old_state = old_state_i;
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER,
+          "supplicant interface state: %s -> %s",
+          nm_supplicant_interface_state_to_string(old_state),
+          nm_supplicant_interface_state_to_string(new_state));
+
+    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
+        supplicant_interface_release(self);
+        wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        if (!supplicant_iface_start(self))
+            return;
+    }
+
+    if (priv->supplicant.is_associated)
+        supplicant_iface_state_is_completed(self, new_state);
+}
+
+static gboolean
+handle_auth_or_fail(NMDeviceEthernet *self, NMActRequest *req, gboolean new_secrets)
+{
+    const char *  setting_name;
+    NMConnection *applied_connection;
+
+    if (!nm_device_auth_retries_try_next(NM_DEVICE(self)))
+        return FALSE;
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_NONE);
+
+    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    setting_name       = nm_connection_need_secrets(applied_connection, NULL);
+    if (!setting_name) {
+        _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets.");
+        return FALSE;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER, "Activation: (ethernet) asking for new secrets");
+
+    /* Don't tear down supplicant if the authentication is optional
+     * because in case of a failure in getting new secrets we want to
+     * keep the supplicant alive.
+     */
+    if (!wired_auth_is_optional(self))
+        supplicant_interface_release(self);
+
+    wired_secrets_get_secrets(
+        self,
+        setting_name,
+        NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+            | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0));
+    return TRUE;
+}
+
+static gboolean
+supplicant_connection_timeout_cb(gpointer user_data)
+{
+    NMDeviceEthernet *       self   = NM_DEVICE_ETHERNET(user_data);
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMDevice *               device = NM_DEVICE(self);
+    NMActRequest *           req;
+    NMSettingsConnection *   connection;
+    guint64                  timestamp   = 0;
+    gboolean                 new_secrets = TRUE;
+
+    priv->supplicant.con_timeout_id = 0;
+
+    /* Authentication failed; either driver problems, the encryption key is
+     * wrong, the passwords or certificates were wrong or the Ethernet switch's
+     * port is not configured for 802.1x. */
+    _LOGW(LOGD_DEVICE | LOGD_ETHER, "Activation: (ethernet) association took too long.");
+
+    req        = nm_device_get_act_request(device);
+    connection = nm_act_request_get_settings_connection(req);
+
+    /* Ask for new secrets only if we've never activated this connection
+     * before.  If we've connected before, don't bother the user with dialogs,
+     * just retry or fail, and if we never connect the user can fix the
+     * password somewhere else. */
+    if (nm_settings_connection_get_timestamp(connection, &timestamp))
+        new_secrets = !timestamp;
+
+    if (!handle_auth_or_fail(self, req, new_secrets)) {
+        wired_auth_cond_fail(self, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return G_SOURCE_REMOVE;
+    }
+
+    if (!priv->supplicant.lnk_timeout_id && priv->supplicant.iface) {
+        NMSupplicantInterfaceState state;
+
+        state = nm_supplicant_interface_get_state(priv->supplicant.iface);
+        if (state != NM_SUPPLICANT_INTERFACE_STATE_COMPLETED
+            && nm_supplicant_interface_state_is_operational(state))
+            priv->supplicant.lnk_timeout_id =
+                g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+supplicant_interface_create_cb(NMSupplicantManager *        supplicant_manager,
+                               NMSupplMgrCreateIfaceHandle *handle,
+                               NMSupplicantInterface *      iface,
+                               GError *                     error,
+                               gpointer                     user_data)
+{
+    NMDeviceEthernet *       self;
+    NMDeviceEthernetPrivate *priv;
+    guint                    timeout;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    nm_assert(priv->supplicant.create_handle == handle);
+    priv->supplicant.create_handle = NULL;
+
+    if (error) {
+        _LOGE(LOGD_DEVICE | LOGD_ETHER,
+              "Couldn't initialize supplicant interface: %s",
+              error->message);
+        supplicant_interface_release(self);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    priv->supplicant.iface         = g_object_ref(iface);
+    priv->supplicant.is_associated = FALSE;
+
+    priv->supplicant.iface_state_id = g_signal_connect(priv->supplicant.iface,
+                                                       NM_SUPPLICANT_INTERFACE_STATE,
+                                                       G_CALLBACK(supplicant_iface_state_cb),
+                                                       self);
+
+    timeout = nm_device_get_supplicant_timeout(NM_DEVICE(self));
+    priv->supplicant.con_timeout_id =
+        g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self);
+
+    if (nm_supplicant_interface_state_is_operational(nm_supplicant_interface_get_state(iface)))
+        supplicant_iface_start(self);
+}
+
+static NMPlatformLinkDuplexType
+link_duplex_to_platform(const char *duplex)
+{
+    if (!duplex)
+        return NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
+    if (nm_streq(duplex, "full"))
+        return NM_PLATFORM_LINK_DUPLEX_FULL;
+    if (nm_streq(duplex, "half"))
+        return NM_PLATFORM_LINK_DUPLEX_HALF;
+    g_return_val_if_reached(NM_PLATFORM_LINK_DUPLEX_UNKNOWN);
+}
+
+static void
+link_negotiation_set(NMDevice *device)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSettingWired *         s_wired;
+    gboolean                 autoneg = TRUE;
+    gboolean                 link_autoneg;
+    NMPlatformLinkDuplexType duplex      = NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
+    NMPlatformLinkDuplexType link_duplex = NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
+    guint32                  speed       = 0;
+    guint32                  link_speed;
+
+    s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED);
+    if (s_wired) {
+        autoneg = nm_setting_wired_get_auto_negotiate(s_wired);
+        speed   = nm_setting_wired_get_speed(s_wired);
+        duplex  = link_duplex_to_platform(nm_setting_wired_get_duplex(s_wired));
+        if (!autoneg && !speed && !duplex) {
+            _LOGD(LOGD_DEVICE, "set-link: ignore link negotiation");
+            return;
+        }
+    }
+
+    if (!nm_platform_ethtool_get_link_settings(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               &link_autoneg,
+                                               &link_speed,
+                                               &link_duplex)) {
+        _LOGW(LOGD_DEVICE, "set-link: unable to retrieve link negotiation");
+        return;
+    }
+
+    /* If link negotiation setting are already in place do nothing and return with success */
+    if (!!autoneg == !!link_autoneg && speed == link_speed && duplex == link_duplex) {
+        _LOGD(LOGD_DEVICE, "set-link: link negotiation is already configured");
+        return;
+    }
+
+    if (autoneg && !speed && !duplex)
+        _LOGD(LOGD_DEVICE, "set-link: configure auto-negotiation");
+    else {
+        _LOGD(LOGD_DEVICE,
+              "set-link: configure %snegotiation (%u Mbit%s, %s duplex%s)",
+              autoneg ? "auto-" : "static ",
+              speed ?: link_speed,
+              speed ? "" : "*",
+              duplex ? nm_platform_link_duplex_type_to_string(duplex)
+                     : nm_platform_link_duplex_type_to_string(link_duplex),
+              duplex ? "" : "*");
+    }
+
+    if (!priv->ethtool_prev_set) {
+        /* remember the values we had before setting it. */
+        priv->ethtool_prev_autoneg = link_autoneg;
+        priv->ethtool_prev_speed   = link_speed;
+        priv->ethtool_prev_duplex  = link_duplex;
+        priv->ethtool_prev_set     = TRUE;
+    }
+
+    if (!nm_platform_ethtool_set_link_settings(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               autoneg,
+                                               speed,
+                                               duplex)) {
+        _LOGW(LOGD_DEVICE, "set-link: failure to set link negotiation");
+        return;
+    }
+}
+
+static gboolean
+pppoe_reconnect_delay(gpointer user_data)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(user_data);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    priv->pppoe_wait_id   = 0;
+    priv->last_pppoe_time = 0;
+    _LOGI(LOGD_DEVICE, "PPPoE reconnect delay complete, resuming connection...");
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+    return G_SOURCE_REMOVE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    if (nm_device_sys_iface_state_is_external_or_assume(device)) {
+        if (!priv->ethtool_prev_set && !nm_device_sys_iface_state_is_external(device)) {
+            NMSettingWired *s_wired;
+
+            /* During restart of NetworkManager service we forget the original auto
+             * negotiation settings. When taking over a device, remember to reset
+             * the "default" during deactivate. */
+            s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED);
+            if (s_wired
+                && (nm_setting_wired_get_auto_negotiate(s_wired)
+                    || nm_setting_wired_get_speed(s_wired)
+                    || nm_setting_wired_get_duplex(s_wired))) {
+                priv->ethtool_prev_set     = TRUE;
+                priv->ethtool_prev_autoneg = TRUE;
+                priv->ethtool_prev_speed   = 0;
+                priv->ethtool_prev_duplex  = NM_PLATFORM_LINK_DUPLEX_UNKNOWN;
+            }
+        }
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    link_negotiation_set(device);
+
+    /* If we're re-activating a PPPoE connection a short while after
+     * a previous PPPoE connection was torn down, wait a bit to allow the
+     * remote side to handle the disconnection.  Otherwise, the peer may
+     * get confused and fail to negotiate the new connection. (rh #1023503)
+     *
+     * FIXME(shutdown): when exiting, we also need to wait before quitting,
+     * at least for additional NM_SHUTDOWN_TIMEOUT_MS seconds because
+     * otherwise after restart the device won't work for the first seconds.
+     */
+    if (priv->last_pppoe_time != 0) {
+        gint32 delay = nm_utils_get_monotonic_timestamp_sec() - priv->last_pppoe_time;
+
+        if (delay < PPPOE_RECONNECT_DELAY
+            && nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE)) {
+            if (priv->pppoe_wait_id == 0) {
+                _LOGI(LOGD_DEVICE,
+                      "delaying PPPoE reconnect for %d seconds to ensure peer is ready...",
+                      delay);
+                priv->pppoe_wait_id = g_timeout_add_seconds(delay, pppoe_reconnect_delay, self);
+            }
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+        nm_clear_g_source(&priv->pppoe_wait_id);
+        priv->last_pppoe_time = 0;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+supplicant_check_secrets_needed(NMDeviceEthernet *self, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMConnection *           connection;
+    NMSetting8021x *         security;
+    const char *             setting_name;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    security = nm_connection_get_setting_802_1x(connection);
+    if (!security) {
+        _LOGE(LOGD_DEVICE, "Invalid or missing 802.1X security");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (!priv->supplicant.mgr)
+        priv->supplicant.mgr = g_object_ref(nm_supplicant_manager_get());
+
+    /* If we need secrets, get them */
+    setting_name = nm_connection_need_secrets(connection, NULL);
+    if (setting_name) {
+        NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self));
+
+        _LOGI(LOGD_DEVICE | LOGD_ETHER,
+              "Activation: (ethernet) connection '%s' has security, but secrets are required.",
+              nm_connection_get_id(connection));
+
+        if (!handle_auth_or_fail(self, req, FALSE)) {
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER,
+          "Activation: (ethernet) connection '%s' requires no security. No secrets needed.",
+          nm_connection_get_id(connection));
+
+    supplicant_interface_release(self);
+
+    priv->supplicant.create_handle =
+        nm_supplicant_manager_create_interface(priv->supplicant.mgr,
+                                               nm_device_get_ifindex(NM_DEVICE(self)),
+                                               NM_SUPPLICANT_DRIVER_WIRED,
+                                               supplicant_interface_create_cb,
+                                               self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+carrier_changed(NMSupplicantInterface *iface, GParamSpec *pspec, NMDeviceEthernet *self)
+{
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMDeviceStateReason      reason;
+    NMActStageReturn         ret;
+
+    if (!nm_device_has_carrier(NM_DEVICE(self)))
+        return;
+
+    _LOGD(LOGD_DEVICE | LOGD_ETHER, "got carrier, initializing supplicant");
+    nm_clear_g_signal_handler(self, &priv->carrier_id);
+    ret = supplicant_check_secrets_needed(self, &reason);
+    if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
+        nm_device_state_changed(NM_DEVICE(self), NM_DEVICE_STATE_FAILED, reason);
+    }
+}
+
+/*****************************************************************************/
+/* PPPoE */
+
+static void
+ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    switch (status) {
+    case NM_PPP_STATUS_DISCONNECT:
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
+        break;
+    case NM_PPP_STATUS_DEAD:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_set_ip_ifindex(device, ifindex)) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    }
+}
+
+static void
+ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Ignore PPP IP4 events that come in after initial configuration */
+    if (nm_device_activate_ip4_state_in_conf(device))
+        nm_device_activate_schedule_ip_config_result(device, AF_INET, NM_IP_CONFIG_CAST(config));
+}
+
+static NMActStageReturn
+pppoe_stage3_ip4_config_start(NMDeviceEthernet *self, NMDeviceStateReason *out_failure_reason)
+{
+    NMDevice *               device = NM_DEVICE(self);
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSettingPppoe *         s_pppoe;
+    NMActRequest *           req;
+    GError *                 err = NULL;
+
+    req = nm_device_get_act_request(device);
+
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_pppoe = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE);
+
+    g_return_val_if_fail(s_pppoe, NM_ACT_STAGE_RETURN_FAILURE);
+
+    priv->ppp_manager = nm_ppp_manager_create(nm_device_get_iface(device), &err);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            nm_device_get_route_table(device, AF_INET),
+                                            nm_device_get_route_metric(device, AF_INET),
+                                            nm_device_get_route_table(device, AF_INET6),
+                                            nm_device_get_route_metric(device, AF_INET6));
+    }
+
+    if (!priv->ppp_manager
+        || !nm_ppp_manager_start(priv->ppp_manager,
+                                 req,
+                                 nm_setting_pppoe_get_username(s_pppoe),
+                                 30,
+                                 0,
+                                 &err)) {
+        _LOGW(LOGD_DEVICE, "PPPoE failed to start: %s", err->message);
+        g_error_free(err);
+
+        g_clear_object(&priv->ppp_manager);
+
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
+                     G_CALLBACK(ppp_state_changed),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
+                     G_CALLBACK(ppp_ifindex_set),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
+                     G_CALLBACK(ppp_ip4_config),
+                     self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static void dcb_state(NMDevice *device, gboolean timeout);
+
+static gboolean
+dcb_carrier_timeout(gpointer user_data)
+{
+    NMDeviceEthernet *       self   = NM_DEVICE_ETHERNET(user_data);
+    NMDeviceEthernetPrivate *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMDevice *               device = NM_DEVICE(user_data);
+
+    g_return_val_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG, G_SOURCE_REMOVE);
+
+    priv->dcb_timeout_id = 0;
+    if (priv->dcb_wait != DCB_WAIT_CARRIER_POSTCONFIG_DOWN) {
+        _LOGW(LOGD_DCB, "DCB: timed out waiting for carrier (step %d)", priv->dcb_wait);
+    }
+    dcb_state(device, TRUE);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+dcb_configure(NMDevice *device)
+{
+    NMDeviceEthernet *       self = (NMDeviceEthernet *) device;
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSettingDcb *           s_dcb;
+    GError *                 error = NULL;
+
+    nm_clear_g_source(&priv->dcb_timeout_id);
+
+    s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB);
+
+    g_return_val_if_fail(s_dcb, FALSE);
+
+    if (!nm_dcb_setup(nm_device_get_iface(device), s_dcb, &error)) {
+        _LOGW(LOGD_DCB, "Activation: (ethernet) failed to enable DCB/FCoE: %s", error->message);
+        g_clear_error(&error);
+        return FALSE;
+    }
+
+    /* Pause again just in case the device takes the carrier down when
+     * setting specific DCB attributes.
+     */
+    _LOGD(LOGD_DCB, "waiting for carrier (postconfig down)");
+    priv->dcb_wait       = DCB_WAIT_CARRIER_POSTCONFIG_DOWN;
+    priv->dcb_timeout_id = g_timeout_add_seconds(3, dcb_carrier_timeout, device);
+    return TRUE;
+}
+
+static gboolean
+dcb_enable(NMDevice *device)
+{
+    NMDeviceEthernet *       self  = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv  = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    GError *                 error = NULL;
+
+    nm_clear_g_source(&priv->dcb_timeout_id);
+    if (!nm_dcb_enable(nm_device_get_iface(device), TRUE, &error)) {
+        _LOGW(LOGD_DCB, "Activation: (ethernet) failed to enable DCB/FCoE: %s", error->message);
+        g_clear_error(&error);
+        return FALSE;
+    }
+
+    /* Pause for 3 seconds after enabling DCB to let the card reconfigure
+     * itself.  Drivers will often re-initialize internal settings which
+     * takes the carrier down for 2 or more seconds.  During this time,
+     * lldpad will refuse to do anything else with the card since the carrier
+     * is down.  But NM might get the carrier-down signal long after calling
+     * "dcbtool dcb on", so we have to first wait for the carrier to go down.
+     */
+    _LOGD(LOGD_DCB, "waiting for carrier (preconfig down)");
+    priv->dcb_wait       = DCB_WAIT_CARRIER_PRECONFIG_DOWN;
+    priv->dcb_timeout_id = g_timeout_add_seconds(3, dcb_carrier_timeout, device);
+    return TRUE;
+}
+
+static void
+dcb_state(NMDevice *device, gboolean timeout)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    gboolean                 carrier;
+
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG);
+
+    carrier = nm_platform_link_is_connected(nm_device_get_platform(device),
+                                            nm_device_get_ifindex(device));
+    _LOGD(LOGD_DCB, "dcb_state() wait %d carrier %d timeout %d", priv->dcb_wait, carrier, timeout);
+
+    switch (priv->dcb_wait) {
+    case DCB_WAIT_CARRIER_PREENABLE_UP:
+        if (timeout || carrier) {
+            _LOGD(LOGD_DCB, "dcb_state() enabling DCB");
+            nm_clear_g_source(&priv->dcb_timeout_id);
+            if (!dcb_enable(device)) {
+                priv->dcb_handle_carrier_changes = FALSE;
+                nm_device_state_changed(device,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED);
+            }
+        }
+        break;
+    case DCB_WAIT_CARRIER_PRECONFIG_DOWN:
+        nm_clear_g_source(&priv->dcb_timeout_id);
+        priv->dcb_wait = DCB_WAIT_CARRIER_PRECONFIG_UP;
+
+        if (!carrier) {
+            /* Wait for the carrier to come back up */
+            _LOGD(LOGD_DCB, "waiting for carrier (preconfig up)");
+            priv->dcb_timeout_id = g_timeout_add_seconds(5, dcb_carrier_timeout, device);
+            break;
+        }
+        _LOGD(LOGD_DCB, "dcb_state() preconfig down falling through");
+        /* fall-through */
+    case DCB_WAIT_CARRIER_PRECONFIG_UP:
+        if (timeout || carrier) {
+            _LOGD(LOGD_DCB, "dcb_state() preconfig up configuring DCB");
+            nm_clear_g_source(&priv->dcb_timeout_id);
+            if (!dcb_configure(device)) {
+                priv->dcb_handle_carrier_changes = FALSE;
+                nm_device_state_changed(device,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED);
+            }
+        }
+        break;
+    case DCB_WAIT_CARRIER_POSTCONFIG_DOWN:
+        nm_clear_g_source(&priv->dcb_timeout_id);
+        priv->dcb_wait = DCB_WAIT_CARRIER_POSTCONFIG_UP;
+
+        if (!carrier) {
+            /* Wait for the carrier to come back up */
+            _LOGD(LOGD_DCB, "waiting for carrier (postconfig up)");
+            priv->dcb_timeout_id = g_timeout_add_seconds(5, dcb_carrier_timeout, device);
+            break;
+        }
+        _LOGD(LOGD_DCB, "dcb_state() postconfig down falling through");
+        /* fall-through */
+    case DCB_WAIT_CARRIER_POSTCONFIG_UP:
+        if (timeout || carrier) {
+            _LOGD(LOGD_DCB, "dcb_state() postconfig up starting IP");
+            nm_clear_g_source(&priv->dcb_timeout_id);
+            priv->dcb_handle_carrier_changes = FALSE;
+            priv->dcb_wait                   = DCB_WAIT_UNKNOWN;
+            nm_device_activate_schedule_stage3_ip_config_start(device);
+        }
+        break;
+    default:
+        g_assert_not_reached();
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+wake_on_lan_enable(NMDevice *device)
+{
+    NMSettingWiredWakeOnLan wol;
+    NMSettingWired *        s_wired;
+    const char *            password = NULL;
+
+    s_wired = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRED);
+
+    if (NM_IS_DEVICE_VETH(device))
+        return FALSE;
+
+    if (s_wired) {
+        wol      = nm_setting_wired_get_wake_on_lan(s_wired);
+        password = nm_setting_wired_get_wake_on_lan_password(s_wired);
+        if (wol != NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT)
+            goto found;
+    }
+
+    wol = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                      NM_CON_DEFAULT("ethernet.wake-on-lan"),
+                                                      device,
+                                                      NM_SETTING_WIRED_WAKE_ON_LAN_NONE,
+                                                      G_MAXINT32,
+                                                      NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT);
+
+    if (NM_FLAGS_ANY(wol, NM_SETTING_WIRED_WAKE_ON_LAN_EXCLUSIVE_FLAGS)
+        && !nm_utils_is_power_of_two(wol)) {
+        nm_log_dbg(LOGD_ETHER, "invalid default value %u for wake-on-lan", (guint) wol);
+        wol = NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT;
+    }
+    if (wol != NM_SETTING_WIRED_WAKE_ON_LAN_DEFAULT)
+        goto found;
+    wol = NM_SETTING_WIRED_WAKE_ON_LAN_IGNORE;
+found:
+    return nm_platform_ethtool_set_wake_on_lan(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               _NM_SETTING_WIRED_WAKE_ON_LAN_CAST(wol),
+                                               password);
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceEthernet *       self = (NMDeviceEthernet *) device;
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSettingConnection *    s_con;
+    const char *             connection_type;
+    gboolean                 do_postpone = FALSE;
+    NMSettingDcb *           s_dcb;
+
+    s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION);
+
+    g_return_val_if_fail(s_con, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_clear_g_source(&priv->dcb_timeout_id);
+    priv->dcb_handle_carrier_changes = FALSE;
+
+    /* 802.1x has to run before any IP configuration since the 802.1x auth
+     * process opens the port up for normal traffic.
+     */
+    connection_type = nm_setting_connection_get_connection_type(s_con);
+    if (nm_streq(connection_type, NM_SETTING_WIRED_SETTING_NAME)) {
+        NMSetting8021x *security;
+
+        security = nm_device_get_applied_setting(device, NM_TYPE_SETTING_802_1X);
+
+        if (security) {
+            /* FIXME: for now 802.1x is mutually exclusive with DCB */
+            if (!nm_device_has_carrier(NM_DEVICE(self))) {
+                _LOGD(LOGD_DEVICE | LOGD_ETHER,
+                      "delay supplicant initialization until carrier goes up");
+                priv->carrier_id = g_signal_connect(self,
+                                                    "notify::" NM_DEVICE_CARRIER,
+                                                    G_CALLBACK(carrier_changed),
+                                                    self);
+                return NM_ACT_STAGE_RETURN_POSTPONE;
+            }
+
+            return supplicant_check_secrets_needed(self, out_failure_reason);
+        }
+    }
+
+    wake_on_lan_enable(device);
+
+    /* DCB and FCoE setup */
+    s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB);
+    if (s_dcb) {
+        /* lldpad really really wants the carrier to be up */
+        if (nm_platform_link_is_connected(nm_device_get_platform(device),
+                                          nm_device_get_ifindex(device))) {
+            if (!dcb_enable(device)) {
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED);
+                return NM_ACT_STAGE_RETURN_FAILURE;
+            }
+        } else {
+            _LOGD(LOGD_DCB, "waiting for carrier (preenable up)");
+            priv->dcb_wait       = DCB_WAIT_CARRIER_PREENABLE_UP;
+            priv->dcb_timeout_id = g_timeout_add_seconds(4, dcb_carrier_timeout, device);
+        }
+
+        priv->dcb_handle_carrier_changes = TRUE;
+        do_postpone                      = TRUE;
+    }
+
+    /* PPPoE setup */
+    if (nm_connection_is_type(nm_device_get_applied_connection(device),
+                              NM_SETTING_PPPOE_SETTING_NAME)) {
+        NMSettingPpp *s_ppp;
+
+        s_ppp = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPP);
+        if (s_ppp) {
+            guint32 mtu;
+            guint32 mru;
+            guint32 mxu;
+
+            mtu = nm_setting_ppp_get_mtu(s_ppp);
+            mru = nm_setting_ppp_get_mru(s_ppp);
+            mxu = MAX(mru, mtu);
+            if (mxu) {
+                _LOGD(LOGD_PPP,
+                      "set MTU to %u (PPP interface MRU %u, MTU %u)",
+                      mxu + PPPOE_ENCAP_OVERHEAD,
+                      mru,
+                      mtu);
+                nm_platform_link_set_mtu(nm_device_get_platform(device),
+                                         nm_device_get_ifindex(device),
+                                         mxu + PPPOE_ENCAP_OVERHEAD);
+            }
+        }
+    }
+
+    return do_postpone ? NM_ACT_STAGE_RETURN_POSTPONE : NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMSettingConnection *s_con;
+    const char *         connection_type;
+    int                  ifindex;
+
+    ifindex = nm_device_get_ifindex(device);
+
+    if (ifindex <= 0)
+        return NM_ACT_STAGE_RETURN_FAILURE;
+
+    if (addr_family == AF_INET) {
+        s_con = nm_device_get_applied_setting(device, NM_TYPE_SETTING_CONNECTION);
+
+        g_return_val_if_fail(s_con, NM_ACT_STAGE_RETURN_FAILURE);
+
+        connection_type = nm_setting_connection_get_connection_type(s_con);
+        if (!strcmp(connection_type, NM_SETTING_PPPOE_SETTING_NAME))
+            return pppoe_stage3_ip4_config_start(NM_DEVICE_ETHERNET(device), out_failure_reason);
+    }
+
+    return NM_DEVICE_CLASS(nm_device_ethernet_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    /* MTU only set for plain ethernet */
+    if (NM_DEVICE_ETHERNET_GET_PRIVATE(device)->ppp_manager)
+        return 0;
+
+    return nm_device_get_configured_mtu_for_wired(device, out_source, out_force);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    NMSettingDcb *           s_dcb;
+    GError *                 error = NULL;
+    int                      ifindex;
+
+    nm_clear_g_source(&priv->pppoe_wait_id);
+    nm_clear_g_signal_handler(self, &priv->carrier_id);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
+        g_clear_object(&priv->ppp_manager);
+    }
+
+    supplicant_interface_release(self);
+
+    priv->dcb_wait = DCB_WAIT_UNKNOWN;
+    nm_clear_g_source(&priv->dcb_timeout_id);
+    priv->dcb_handle_carrier_changes = FALSE;
+
+    /* Tear down DCB/FCoE if it was enabled */
+    s_dcb = nm_device_get_applied_setting(device, NM_TYPE_SETTING_DCB);
+    if (s_dcb) {
+        if (!nm_dcb_cleanup(nm_device_get_iface(device), &error)) {
+            _LOGW(LOGD_DEVICE | LOGD_PLATFORM, "failed to disable DCB/FCoE: %s", error->message);
+            g_clear_error(&error);
+        }
+    }
+
+    /* Set last PPPoE connection time */
+    if (nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE))
+        priv->last_pppoe_time = nm_utils_get_monotonic_timestamp_sec();
+
+    ifindex = nm_device_get_ifindex(device);
+    if (ifindex > 0 && priv->ethtool_prev_set) {
+        priv->ethtool_prev_set = FALSE;
+
+        _LOGD(LOGD_DEVICE,
+              "set-link: reset %snegotiation (%u Mbit, %s duplex)",
+              priv->ethtool_prev_autoneg ? "auto-" : "static ",
+              priv->ethtool_prev_speed,
+              nm_platform_link_duplex_type_to_string(priv->ethtool_prev_duplex));
+        if (!nm_platform_ethtool_set_link_settings(nm_device_get_platform(device),
+                                                   ifindex,
+                                                   priv->ethtool_prev_autoneg,
+                                                   priv->ethtool_prev_speed,
+                                                   priv->ethtool_prev_duplex)) {
+            _LOGW(LOGD_DEVICE, "set-link: failure to reset link negotiation");
+            return;
+        }
+    }
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingWired *s_wired;
+    NMSettingPppoe *s_pppoe;
+
+    if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_VETH_SETTING_NAME)) {
+        NMSettingVeth *s_veth;
+        const char *   peer_name     = NULL;
+        const char *   con_peer_name = NULL;
+        int            ifindex;
+
+        nm_utils_complete_generic(nm_device_get_platform(device),
+                                  connection,
+                                  NM_SETTING_VETH_SETTING_NAME,
+                                  existing_connections,
+                                  NULL,
+                                  _("Veth connection"),
+                                  "veth",
+                                  NULL,
+                                  TRUE);
+
+        s_veth = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VETH);
+        if (!s_veth) {
+            s_veth = (NMSettingVeth *) nm_setting_veth_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_veth));
+        }
+
+        ifindex = nm_device_get_ip_ifindex(device);
+        if (ifindex > 0) {
+            const NMPlatformLink *pllink;
+
+            pllink = nm_platform_link_get(nm_device_get_platform(device), ifindex);
+            if (pllink && pllink->type == NM_LINK_TYPE_VETH && pllink->parent > 0) {
+                pllink = nm_platform_link_get(nm_device_get_platform(device), pllink->parent);
+
+                if (pllink && pllink->type == NM_LINK_TYPE_VETH) {
+                    peer_name = pllink->name;
+                }
+            }
+        }
+
+        if (!peer_name) {
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "cannot find peer for veth device");
+            return FALSE;
+        }
+
+        con_peer_name = nm_setting_veth_get_peer(s_veth);
+        if (con_peer_name) {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_UNKNOWN,
+                               "mismatching veth peer \"%s\"",
+                               con_peer_name);
+            return FALSE;
+        } else
+            g_object_set(s_veth, NM_SETTING_VETH_PEER, peer_name, NULL);
+
+        return TRUE;
+    }
+
+    s_pppoe = nm_connection_get_setting_pppoe(connection);
+
+    /* We can't telepathically figure out the service name or username, so if
+     * those weren't given, we can't complete the connection.
+     */
+    if (s_pppoe && !nm_setting_verify(NM_SETTING(s_pppoe), NULL, error))
+        return FALSE;
+
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (!s_wired) {
+        s_wired = (NMSettingWired *) nm_setting_wired_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wired));
+    }
+
+    /* Default to an ethernet-only connection, but if a PPPoE setting was given
+     * then PPPoE should be our connection type.
+     */
+    nm_utils_complete_generic(
+        nm_device_get_platform(device),
+        connection,
+        s_pppoe ? NM_SETTING_PPPOE_SETTING_NAME : NM_SETTING_WIRED_SETTING_NAME,
+        existing_connections,
+        NULL,
+        s_pppoe ? _("PPPoE connection") : _("Wired connection"),
+        NULL,
+        nm_setting_wired_get_mac_address(s_wired) ? NULL : nm_device_get_iface(device),
+        s_pppoe ? FALSE : TRUE); /* No IPv6 by default yet for PPPoE */
+
+    return TRUE;
+}
+
+static NMConnection *
+new_default_connection(NMDevice *self)
+{
+    NMConnection *               connection;
+    NMSettingsConnection *const *connections;
+    NMSetting *                  setting;
+    gs_unref_hashtable GHashTable *existing_ids = NULL;
+    struct udev_device *           dev;
+    const char *                   perm_hw_addr;
+    const char *                   iface;
+    const char *                   uprop   = "0";
+    gs_free char *                 defname = NULL;
+    gs_free char *                 uuid    = NULL;
+    guint                          i, n_connections;
+
+    perm_hw_addr = nm_device_get_permanent_hw_address(self);
+    iface        = nm_device_get_iface(self);
+
+    connection = nm_simple_connection_new();
+    setting    = nm_setting_connection_new();
+    nm_connection_add_setting(connection, setting);
+
+    connections = nm_settings_get_connections(nm_device_get_settings(self), &n_connections);
+    if (n_connections > 0) {
+        existing_ids = g_hash_table_new(nm_str_hash, g_str_equal);
+        for (i = 0; i < n_connections; i++)
+            g_hash_table_add(existing_ids, (char *) nm_settings_connection_get_id(connections[i]));
+    }
+    defname = nm_device_ethernet_utils_get_default_wired_name(existing_ids);
+    if (!defname)
+        return NULL;
+
+    /* Create a stable UUID. The UUID is also the Network_ID for stable-privacy addr-gen-mode,
+     * thus when it changes we will also generate different IPv6 addresses. */
+    uuid = _nm_utils_uuid_generate_from_strings("default-wired",
+                                                nm_utils_machine_id_str(),
+                                                defname,
+                                                perm_hw_addr ?: iface,
+                                                NULL);
+
+    g_object_set(setting,
+                 NM_SETTING_CONNECTION_ID,
+                 defname,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_WIRED_SETTING_NAME,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 TRUE,
+                 NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY,
+                 NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY_MIN,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_TIMESTAMP,
+                 (guint64) time(NULL),
+                 NM_SETTING_CONNECTION_INTERFACE_NAME,
+                 iface,
+                 NULL);
+
+    /* Check if we should create a Link-Local only connection */
+    dev = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)),
+                                           nm_device_get_ip_ifindex(self));
+    if (dev)
+        uprop = udev_device_get_property_value(dev, "NM_AUTO_DEFAULT_LINK_LOCAL_ONLY");
+
+    if (nm_udev_utils_property_as_boolean(uprop)) {
+        setting = nm_setting_ip4_config_new();
+        g_object_set(setting,
+                     NM_SETTING_IP_CONFIG_METHOD,
+                     NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL,
+                     NULL);
+        nm_connection_add_setting(connection, setting);
+
+        setting = nm_setting_ip6_config_new();
+        g_object_set(setting,
+                     NM_SETTING_IP_CONFIG_METHOD,
+                     NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL,
+                     NM_SETTING_IP_CONFIG_MAY_FAIL,
+                     TRUE,
+                     NULL);
+        nm_connection_add_setting(connection, setting);
+    }
+
+    return connection;
+}
+
+static const char *
+get_s390_subchannels(NMDevice *device)
+{
+    nm_assert(NM_IS_DEVICE_ETHERNET(device));
+
+    return NM_DEVICE_ETHERNET_GET_PRIVATE(device)->subchannels;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceEthernetPrivate *priv    = NM_DEVICE_ETHERNET_GET_PRIVATE(device);
+    NMSettingWired *         s_wired = nm_connection_get_setting_wired(connection);
+    gboolean                 perm_hw_addr_is_fake;
+    const char *             perm_hw_addr;
+    const char *             mac      = nm_device_get_hw_address(device);
+    const char *             mac_prop = NM_SETTING_WIRED_MAC_ADDRESS;
+    GHashTableIter           iter;
+    gpointer                 key, value;
+
+    if (!s_wired) {
+        s_wired = (NMSettingWired *) nm_setting_wired_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_wired);
+    }
+
+    g_object_set(nm_connection_get_setting_connection(connection),
+                 NM_SETTING_CONNECTION_TYPE,
+                 nm_connection_get_setting_pppoe(connection) ? NM_SETTING_PPPOE_SETTING_NAME
+                                                             : NM_SETTING_WIRED_SETTING_NAME,
+                 NULL);
+
+    /* If the device reports a permanent address, use that for the MAC address
+     * and the current MAC, if different, is the cloned MAC.
+     */
+    perm_hw_addr = nm_device_get_permanent_hw_address_full(device, TRUE, &perm_hw_addr_is_fake);
+    if (perm_hw_addr && !perm_hw_addr_is_fake) {
+        g_object_set(s_wired, NM_SETTING_WIRED_MAC_ADDRESS, perm_hw_addr, NULL);
+
+        mac_prop = NULL;
+        if (mac && !nm_utils_hwaddr_matches(perm_hw_addr, -1, mac, -1))
+            mac_prop = NM_SETTING_WIRED_CLONED_MAC_ADDRESS;
+    }
+
+    if (mac_prop && mac && nm_utils_hwaddr_valid(mac, ETH_ALEN))
+        g_object_set(s_wired, mac_prop, mac, NULL);
+
+    /* We don't set the MTU as we don't know whether it was set explicitly */
+
+    /* s390 */
+    if (priv->subchannels_dbus)
+        g_object_set(s_wired, NM_SETTING_WIRED_S390_SUBCHANNELS, priv->subchannels_dbus, NULL);
+    if (priv->s390_nettype)
+        g_object_set(s_wired, NM_SETTING_WIRED_S390_NETTYPE, priv->s390_nettype, NULL);
+
+    _nm_setting_wired_clear_s390_options(s_wired);
+    g_hash_table_iter_init(&iter, priv->s390_options);
+    while (g_hash_table_iter_next(&iter, &key, &value))
+        nm_setting_wired_add_s390_option(s_wired, (const char *) key, (const char *) value);
+}
+
+static void
+link_speed_update(NMDevice *device)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+    guint32                  speed;
+
+    if (!nm_platform_ethtool_get_link_settings(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               NULL,
+                                               &speed,
+                                               NULL))
+        return;
+    if (priv->speed == speed)
+        return;
+
+    priv->speed = speed;
+    _LOGD(LOGD_PLATFORM | LOGD_ETHER, "speed is now %d Mb/s", speed);
+    _notify(self, PROP_SPEED);
+}
+
+static void
+carrier_changed_notify(NMDevice *device, gboolean carrier)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(device);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    if (priv->dcb_handle_carrier_changes) {
+        nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG);
+
+        if (priv->dcb_timeout_id) {
+            _LOGD(LOGD_DCB, "carrier_changed() calling dcb_state()");
+            dcb_state(device, FALSE);
+        }
+    }
+
+    if (carrier)
+        link_speed_update(device);
+
+    NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->carrier_changed_notify(device, carrier);
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->link_changed(device, pllink);
+    if (!NM_IS_DEVICE_VETH(device) && pllink->initialized)
+        _update_s390_subchannels((NMDeviceEthernet *) device);
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    if (!NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->is_available(device, flags))
+        return FALSE;
+
+    return !!nm_device_get_initial_hw_address(device);
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    /* Only handle wired setting here, delegate other settings to parent class */
+    if (nm_streq(setting_name, NM_SETTING_WIRED_SETTING_NAME)) {
+        return nm_device_hash_check_invalid_keys(
+            diffs,
+            NM_SETTING_WIRED_SETTING_NAME,
+            error,
+            NM_SETTING_WIRED_MTU, /* reapplied with IP config */
+            NM_SETTING_WIRED_SPEED,
+            NM_SETTING_WIRED_DUPLEX,
+            NM_SETTING_WIRED_AUTO_NEGOTIATE,
+            NM_SETTING_WIRED_WAKE_ON_LAN,
+            NM_SETTING_WIRED_WAKE_ON_LAN_PASSWORD);
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_ethernet_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static void
+reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceEthernet *self  = NM_DEVICE_ETHERNET(device);
+    NMDeviceState     state = nm_device_get_state(device);
+
+    NM_DEVICE_CLASS(nm_device_ethernet_parent_class)->reapply_connection(device, con_old, con_new);
+
+    _LOGD(LOGD_DEVICE, "reapplying wired settings");
+
+    if (state >= NM_DEVICE_STATE_PREPARE)
+        link_negotiation_set(device);
+    if (state >= NM_DEVICE_STATE_CONFIG)
+        wake_on_lan_enable(device);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(object);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    wired_secrets_cancel(self);
+
+    supplicant_interface_release(self);
+
+    nm_clear_g_source(&priv->pppoe_wait_id);
+
+    nm_clear_g_source(&priv->dcb_timeout_id);
+
+    nm_clear_g_signal_handler(self, &priv->carrier_id);
+
+    G_OBJECT_CLASS(nm_device_ethernet_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(object);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    g_clear_object(&priv->supplicant.mgr);
+    g_free(priv->subchan1);
+    g_free(priv->subchan2);
+    g_free(priv->subchan3);
+    g_free(priv->subchannels);
+    g_strfreev(priv->subchannels_dbus);
+    g_free(priv->s390_nettype);
+    g_hash_table_destroy(priv->s390_options);
+
+    G_OBJECT_CLASS(nm_device_ethernet_parent_class)->finalize(object);
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceEthernet *       self = NM_DEVICE_ETHERNET(object);
+    NMDeviceEthernetPrivate *priv = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_SPEED:
+        g_value_set_uint(value, priv->speed);
+        break;
+    case PROP_S390_SUBCHANNELS:
+        g_value_set_boxed(value, priv->subchannels_dbus);
+        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)
+{
+    switch (prop_id) {
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_wired = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_WIRED,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PermHwAddress",
+                                                             "s",
+                                                             NM_DEVICE_PERM_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Speed",
+                                                             "u",
+                                                             NM_DEVICE_ETHERNET_SPEED),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("S390Subchannels",
+                                                             "as",
+                                                             NM_DEVICE_ETHERNET_S390_SUBCHANNELS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier",
+                                                             "b",
+                                                             NM_DEVICE_CARRIER), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ethernet_class_init(NMDeviceEthernetClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMDeviceEthernetPrivate));
+
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wired);
+
+    device_class->connection_type_supported = NM_SETTING_WIRED_SETTING_NAME;
+    device_class->link_types                = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_ETHERNET);
+
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+    device_class->new_default_connection      = new_default_connection;
+
+    device_class->act_stage1_prepare_also_for_external_or_assume = TRUE;
+    device_class->act_stage1_prepare                             = act_stage1_prepare;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet         = TRUE;
+    device_class->act_stage2_config                              = act_stage2_config;
+    device_class->act_stage3_ip_config_start                     = act_stage3_ip_config_start;
+    device_class->get_configured_mtu                             = get_configured_mtu;
+    device_class->deactivate                                     = deactivate;
+    device_class->get_s390_subchannels                           = get_s390_subchannels;
+    device_class->update_connection                              = update_connection;
+    device_class->carrier_changed_notify                         = carrier_changed_notify;
+    device_class->link_changed                                   = link_changed;
+    device_class->is_available                                   = is_available;
+    device_class->can_reapply_change                             = can_reapply_change;
+    device_class->reapply_connection                             = reapply_connection;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_SPEED] = g_param_spec_uint(NM_DEVICE_ETHERNET_SPEED,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT32,
+                                                   0,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_S390_SUBCHANNELS] =
+        g_param_spec_boxed(NM_DEVICE_ETHERNET_S390_SUBCHANNELS,
+                           "",
+                           "",
+                           G_TYPE_STRV,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_ETHERNET_DEVICE_FACTORY (nm_ethernet_device_factory_get_type())
+#define NM_ETHERNET_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_ETHERNET_DEVICE_FACTORY, NMEthernetDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_ETHERNET,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Ethernet",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_ETHERNET,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_ETHERNET,
+                        NULL);
+}
+
+static gboolean
+match_connection(NMDeviceFactory *factory, NMConnection *connection)
+{
+    const char *    type = nm_connection_get_connection_type(connection);
+    NMSettingPppoe *s_pppoe;
+
+    if (nm_streq(type, NM_SETTING_WIRED_SETTING_NAME))
+        return TRUE;
+
+    nm_assert(nm_streq(type, NM_SETTING_PPPOE_SETTING_NAME));
+    s_pppoe = nm_connection_get_setting_pppoe(connection);
+
+    return !nm_setting_pppoe_get_parent(s_pppoe);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    ETHERNET,
+    Ethernet,
+    ethernet,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_ETHERNET)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIRED_SETTING_NAME,
+                                                NM_SETTING_PPPOE_SETTING_NAME),
+    factory_class->create_device    = create_device;
+    factory_class->match_connection = match_connection;);
diff --git a/src/core/devices/nm-device-ethernet.h b/src/core/devices/nm-device-ethernet.h
new file mode 100644
index 00000000..6e134d71
--- /dev/null
+++ b/src/core/devices/nm-device-ethernet.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2010 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_ETHERNET_H__
+#define __NETWORKMANAGER_DEVICE_ETHERNET_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_ETHERNET (nm_device_ethernet_get_type())
+#define NM_DEVICE_ETHERNET(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernet))
+#define NM_DEVICE_ETHERNET_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetClass))
+#define NM_IS_DEVICE_ETHERNET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_ETHERNET))
+#define NM_IS_DEVICE_ETHERNET_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_ETHERNET))
+#define NM_DEVICE_ETHERNET_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_ETHERNET, NMDeviceEthernetClass))
+
+#define NM_DEVICE_ETHERNET_SPEED            "speed"
+#define NM_DEVICE_ETHERNET_S390_SUBCHANNELS "s390-subchannels"
+
+struct _NMDeviceEthernetPrivate;
+
+typedef struct {
+    NMDevice                         parent;
+    struct _NMDeviceEthernetPrivate *_priv;
+} NMDeviceEthernet;
+
+typedef struct {
+    NMDeviceClass parent;
+} NMDeviceEthernetClass;
+
+GType nm_device_ethernet_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_ETHERNET_H__ */
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
new file mode 100644
index 00000000..81124a8d
--- /dev/null
+++ b/src/core/devices/nm-device-factory.c
@@ -0,0 +1,413 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 - 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-factory.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <gmodule.h>
+
+#include "platform/nm-platform.h"
+#include "nm-utils.h"
+#include "nm-core-internal.h"
+#include "nm-setting-bluetooth.h"
+
+#define PLUGIN_PREFIX "libnm-device-plugin-"
+
+/*****************************************************************************/
+
+enum { DEVICE_ADDED, LAST_SIGNAL };
+
+static guint signals[LAST_SIGNAL] = {0};
+
+G_DEFINE_ABSTRACT_TYPE(NMDeviceFactory, nm_device_factory, G_TYPE_OBJECT)
+
+/*****************************************************************************/
+
+static void
+nm_device_factory_get_supported_types(NMDeviceFactory *   factory,
+                                      const NMLinkType ** out_link_types,
+                                      const char *const **out_setting_types)
+{
+    g_return_if_fail(NM_IS_DEVICE_FACTORY(factory));
+
+    NM_DEVICE_FACTORY_GET_CLASS(factory)->get_supported_types(factory,
+                                                              out_link_types,
+                                                              out_setting_types);
+}
+
+void
+nm_device_factory_start(NMDeviceFactory *factory)
+{
+    g_return_if_fail(factory != NULL);
+
+    if (NM_DEVICE_FACTORY_GET_CLASS(factory)->start)
+        NM_DEVICE_FACTORY_GET_CLASS(factory)->start(factory);
+}
+
+NMDevice *
+nm_device_factory_create_device(NMDeviceFactory *     factory,
+                                const char *          iface,
+                                const NMPlatformLink *plink,
+                                NMConnection *        connection,
+                                gboolean *            out_ignore,
+                                GError **             error)
+{
+    NMDeviceFactoryClass *klass;
+    NMDevice *            device;
+    gboolean              ignore = FALSE;
+
+    g_return_val_if_fail(factory, NULL);
+    g_return_val_if_fail(iface && *iface, NULL);
+    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));
+    } else if (connection)
+        nm_assert(factory == nm_device_factory_manager_find_factory_for_connection(connection));
+    else
+        g_return_val_if_reached(NULL);
+
+    klass = NM_DEVICE_FACTORY_GET_CLASS(factory);
+    if (!klass->create_device) {
+        g_set_error(error,
+                    NM_MANAGER_ERROR,
+                    NM_MANAGER_ERROR_FAILED,
+                    "Device factory %s cannot manage new devices",
+                    G_OBJECT_TYPE_NAME(factory));
+        NM_SET_OUT(out_ignore, FALSE);
+        return NULL;
+    }
+
+    device = klass->create_device(factory, iface, plink, connection, &ignore);
+    NM_SET_OUT(out_ignore, ignore);
+    if (!device) {
+        if (ignore) {
+            g_set_error(error,
+                        NM_MANAGER_ERROR,
+                        NM_MANAGER_ERROR_FAILED,
+                        "Device factory %s ignores device %s",
+                        G_OBJECT_TYPE_NAME(factory),
+                        iface);
+        } else {
+            g_set_error(error,
+                        NM_MANAGER_ERROR,
+                        NM_MANAGER_ERROR_FAILED,
+                        "Device factory %s failed to create device %s",
+                        G_OBJECT_TYPE_NAME(factory),
+                        iface);
+        }
+    }
+    return device;
+}
+
+const char *
+nm_device_factory_get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    g_return_val_if_fail(factory != NULL, NULL);
+    g_return_val_if_fail(connection != NULL, NULL);
+
+    if (!nm_connection_is_virtual(connection))
+        return NULL;
+
+    if (NM_DEVICE_FACTORY_GET_CLASS(factory)->get_connection_parent)
+        return NM_DEVICE_FACTORY_GET_CLASS(factory)->get_connection_parent(factory, connection);
+    return NULL;
+}
+
+char *
+nm_device_factory_get_connection_iface(NMDeviceFactory *factory,
+                                       NMConnection *   connection,
+                                       const char *     parent_iface,
+                                       GError **        error)
+{
+    NMDeviceFactoryClass *klass;
+    char *                ifname;
+
+    g_return_val_if_fail(factory != NULL, NULL);
+    g_return_val_if_fail(connection != NULL, NULL);
+    g_return_val_if_fail(!error || !*error, NULL);
+
+    klass = NM_DEVICE_FACTORY_GET_CLASS(factory);
+
+    ifname = g_strdup(nm_connection_get_interface_name(connection));
+    if (!ifname && klass->get_connection_iface)
+        ifname = klass->get_connection_iface(factory, connection, parent_iface);
+
+    if (!ifname) {
+        g_set_error(error,
+                    NM_MANAGER_ERROR,
+                    NM_MANAGER_ERROR_FAILED,
+                    "failed to determine interface name: error determine name for %s",
+                    nm_connection_get_connection_type(connection));
+        return NULL;
+    }
+
+    return ifname;
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_factory_init(NMDeviceFactory *self)
+{}
+
+static void
+nm_device_factory_class_init(NMDeviceFactoryClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    signals[DEVICE_ADDED] = g_signal_new(NM_DEVICE_FACTORY_DEVICE_ADDED,
+                                         G_OBJECT_CLASS_TYPE(object_class),
+                                         G_SIGNAL_RUN_FIRST,
+                                         0,
+                                         NULL,
+                                         NULL,
+                                         NULL,
+                                         G_TYPE_NONE,
+                                         1,
+                                         NM_TYPE_DEVICE);
+}
+
+/*****************************************************************************/
+
+static GHashTable *factories_by_link    = NULL;
+static GHashTable *factories_by_setting = NULL;
+
+static void __attribute__((destructor)) _cleanup(void)
+{
+    nm_clear_pointer(&factories_by_link, g_hash_table_unref);
+    nm_clear_pointer(&factories_by_setting, g_hash_table_unref);
+}
+
+NMDeviceFactory *
+nm_device_factory_manager_find_factory_for_link_type(NMLinkType link_type)
+{
+    g_return_val_if_fail(factories_by_link, NULL);
+
+    return g_hash_table_lookup(factories_by_link, GUINT_TO_POINTER(link_type));
+}
+
+NMDeviceFactory *
+nm_device_factory_manager_find_factory_for_connection(NMConnection *connection)
+{
+    NMDeviceFactoryClass *klass;
+    NMDeviceFactory *     factory;
+    const char *          type;
+    GSList *              list;
+
+    g_return_val_if_fail(factories_by_setting, NULL);
+
+    type = nm_connection_get_connection_type(connection);
+    list = g_hash_table_lookup(factories_by_setting, type);
+
+    for (; list; list = g_slist_next(list)) {
+        factory = list->data;
+        klass   = NM_DEVICE_FACTORY_GET_CLASS(factory);
+        if (!klass->match_connection || klass->match_connection(factory, connection))
+            return factory;
+    }
+
+    return NULL;
+}
+
+void
+nm_device_factory_manager_for_each_factory(NMDeviceFactoryManagerFactoryFunc callback,
+                                           gpointer                          user_data)
+{
+    GHashTableIter   iter;
+    NMDeviceFactory *factory;
+    GSList *         list_iter, *list = NULL;
+
+    if (factories_by_link) {
+        g_hash_table_iter_init(&iter, factories_by_link);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer) &factory)) {
+            if (!g_slist_find(list, factory))
+                list = g_slist_prepend(list, factory);
+        }
+    }
+
+    if (factories_by_setting) {
+        g_hash_table_iter_init(&iter, factories_by_setting);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer) &list_iter)) {
+            for (; list_iter; list_iter = g_slist_next(list_iter)) {
+                if (!g_slist_find(list, list_iter->data))
+                    list = g_slist_prepend(list, list_iter->data);
+            }
+        }
+    }
+
+    for (list_iter = list; list_iter; list_iter = list_iter->next)
+        callback(list_iter->data, user_data);
+
+    g_slist_free(list);
+}
+
+static gboolean
+_add_factory(NMDeviceFactory *                 factory,
+             const char *                      path,
+             NMDeviceFactoryManagerFactoryFunc callback,
+             gpointer                          user_data)
+{
+    const NMLinkType * link_types    = NULL;
+    const char *const *setting_types = NULL;
+    GSList *           list, *list2;
+    int                i;
+
+    g_return_val_if_fail(factories_by_link, FALSE);
+    g_return_val_if_fail(factories_by_setting, FALSE);
+
+    nm_device_factory_get_supported_types(factory, &link_types, &setting_types);
+
+    g_return_val_if_fail((link_types && link_types[0] > NM_LINK_TYPE_UNKNOWN)
+                             || (setting_types && setting_types[0]),
+                         FALSE);
+
+    for (i = 0; link_types && link_types[i] > NM_LINK_TYPE_UNKNOWN; i++)
+        g_hash_table_insert(factories_by_link,
+                            GUINT_TO_POINTER(link_types[i]),
+                            g_object_ref(factory));
+    for (i = 0; setting_types && setting_types[i]; i++) {
+        list = g_hash_table_lookup(factories_by_setting, (char *) setting_types[i]);
+        if (list) {
+            list2 = g_slist_append(list, g_object_ref(factory));
+            nm_assert(list == list2);
+        } else {
+            list = g_slist_append(list, g_object_ref(factory));
+            g_hash_table_insert(factories_by_setting, (char *) setting_types[i], list);
+        }
+    }
+
+    callback(factory, user_data);
+
+    nm_log(path ? LOGL_INFO : LOGL_DEBUG,
+           LOGD_PLATFORM,
+           NULL,
+           NULL,
+           "Loaded device plugin: %s (%s)",
+           G_OBJECT_TYPE_NAME(factory),
+           path ?: "internal");
+    return TRUE;
+}
+
+static void
+_load_internal_factory(GType                             factory_gtype,
+                       NMDeviceFactoryManagerFactoryFunc callback,
+                       gpointer                          user_data)
+{
+    gs_unref_object NMDeviceFactory *factory = NULL;
+
+    factory = g_object_new(factory_gtype, NULL);
+    _add_factory(factory, NULL, callback, user_data);
+}
+
+static void
+factories_list_unref(GSList *list)
+{
+    g_slist_free_full(list, g_object_unref);
+}
+
+static void
+load_factories_from_dir(const char *                      dirname,
+                        NMDeviceFactoryManagerFactoryFunc callback,
+                        gpointer                          user_data)
+{
+    NMDeviceFactory *factory;
+    GError *         error = NULL;
+    char **          path, **paths;
+
+    paths = nm_utils_read_plugin_paths(dirname, PLUGIN_PREFIX);
+    if (!paths)
+        return;
+
+    for (path = paths; *path; path++) {
+        GModule *                 plugin;
+        NMDeviceFactoryCreateFunc create_func;
+        const char *              item;
+
+        item = strrchr(*path, '/');
+        g_assert(item);
+
+        plugin = g_module_open(*path, G_MODULE_BIND_LOCAL);
+
+        if (!plugin) {
+            nm_log_warn(LOGD_PLATFORM, "(%s): failed to load plugin: %s", item, g_module_error());
+            continue;
+        }
+
+        if (!g_module_symbol(plugin, "nm_device_factory_create", (gpointer) &create_func)) {
+            nm_log_warn(LOGD_PLATFORM,
+                        "(%s): failed to find device factory creator: %s",
+                        item,
+                        g_module_error());
+            g_module_close(plugin);
+            continue;
+        }
+
+        /* after loading glib types from the plugin, we cannot unload the library anymore.
+         * Make it resident. */
+        g_module_make_resident(plugin);
+
+        factory = create_func(&error);
+        if (!factory) {
+            nm_log_warn(LOGD_PLATFORM,
+                        "(%s): failed to initialize device factory: %s",
+                        item,
+                        NM_G_ERROR_MSG(error));
+            g_clear_error(&error);
+            continue;
+        }
+        g_clear_error(&error);
+
+        _add_factory(factory, g_module_name(plugin), callback, user_data);
+
+        g_object_unref(factory);
+    }
+    g_strfreev(paths);
+}
+
+void
+nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callback,
+                                         gpointer                          user_data)
+{
+    g_return_if_fail(factories_by_link == NULL);
+    g_return_if_fail(factories_by_setting == NULL);
+
+    factories_by_link    = g_hash_table_new_full(nm_direct_hash, NULL, NULL, g_object_unref);
+    factories_by_setting = g_hash_table_new_full(nm_str_hash,
+                                                 g_str_equal,
+                                                 NULL,
+                                                 (GDestroyNotify) factories_list_unref);
+
+#define _ADD_INTERNAL(get_type_fcn)                                  \
+    G_STMT_START                                                     \
+    {                                                                \
+        GType get_type_fcn(void);                                    \
+        _load_internal_factory(get_type_fcn(), callback, user_data); \
+    }                                                                \
+    G_STMT_END
+
+    _ADD_INTERNAL(nm_6lowpan_device_factory_get_type);
+    _ADD_INTERNAL(nm_bond_device_factory_get_type);
+    _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_infiniband_device_factory_get_type);
+    _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type);
+    _ADD_INTERNAL(nm_macsec_device_factory_get_type);
+    _ADD_INTERNAL(nm_macvlan_device_factory_get_type);
+    _ADD_INTERNAL(nm_ppp_device_factory_get_type);
+    _ADD_INTERNAL(nm_tun_device_factory_get_type);
+    _ADD_INTERNAL(nm_veth_device_factory_get_type);
+    _ADD_INTERNAL(nm_vlan_device_factory_get_type);
+    _ADD_INTERNAL(nm_vrf_device_factory_get_type);
+    _ADD_INTERNAL(nm_vxlan_device_factory_get_type);
+    _ADD_INTERNAL(nm_wireguard_device_factory_get_type);
+    _ADD_INTERNAL(nm_wpan_device_factory_get_type);
+
+    load_factories_from_dir(NMPLUGINDIR, callback, user_data);
+}
diff --git a/src/core/devices/nm-device-factory.h b/src/core/devices/nm-device-factory.h
new file mode 100644
index 00000000..d9b50563
--- /dev/null
+++ b/src/core/devices/nm-device-factory.h
@@ -0,0 +1,237 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2007 - 2014 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_FACTORY_H__
+#define __NETWORKMANAGER_DEVICE_FACTORY_H__
+
+#include "nm-dbus-interface.h"
+#include "nm-device.h"
+
+/* WARNING: this file is private API between NetworkManager and its internal
+ * device plugins.  Its API can change at any time and is not guaranteed to be
+ * stable.  NM and device plugins are distributed together and this API is
+ * not meant to enable third-party plugins.
+ */
+
+#define NM_TYPE_DEVICE_FACTORY (nm_device_factory_get_type())
+#define NM_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactory))
+#define NM_DEVICE_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_FACTORY, NMDeviceFactoryClass))
+#define NM_IS_DEVICE_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_FACTORY))
+#define NM_IS_DEVICE_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_FACTORY))
+#define NM_DEVICE_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_FACTORY, NMDeviceFactoryClass))
+
+#define NM_DEVICE_FACTORY_DEVICE_ADDED "device-added"
+
+typedef struct {
+    GObject parent;
+} NMDeviceFactory;
+
+typedef struct {
+    GObjectClass parent;
+
+    /**
+     * get_supported_types:
+     * @factory: the #NMDeviceFactory
+     * @out_link_types: on return, a %NM_LINK_TYPE_NONE terminated
+     *  list of #NMLinkType that the plugin supports
+     * @out_setting_types: on return, a %NULL terminated list of
+     *  base-type #NMSetting names that the plugin can create devices for
+     *
+     * Returns the #NMLinkType and #NMSetting names that this plugin
+     * supports.  This function MUST be implemented.
+     */
+    void (*get_supported_types)(NMDeviceFactory *   factory,
+                                const NMLinkType ** out_link_types,
+                                const char *const **out_setting_types);
+
+    /**
+     * start:
+     * @factory: the #NMDeviceFactory
+     *
+     * Start the factory and discover any existing devices that the factory
+     * can manage.
+     */
+    void (*start)(NMDeviceFactory *factory);
+
+    /**
+     * match_connection:
+     * @connection: the #NMConnection
+     *
+     * Check if the factory supports the given connection.
+     */
+    gboolean (*match_connection)(NMDeviceFactory *factory, NMConnection *connection);
+
+    /**
+     * get_connection_parent:
+     * @factory: the #NMDeviceFactory
+     * @connection: the #NMConnection to return the parent name for, if supported
+     *
+     * Given a connection, returns the parent interface name, parent connection
+     * UUID, or parent device permanent hardware address for @connection.
+     *
+     * Returns: the parent interface name, parent connection UUID, parent
+     *   device permanent hardware address, or %NULL
+     */
+    const char *(*get_connection_parent)(NMDeviceFactory *factory, NMConnection *connection);
+
+    /**
+     * get_connection_iface:
+     * @factory: the #NMDeviceFactory
+     * @connection: the #NMConnection to return the interface name for
+     * @parent_iface: optional parent interface name for virtual devices
+     *
+     * Given a connection, returns the interface name that a device activating
+     * that connection would have.
+     *
+     * Returns: the interface name, or %NULL
+     */
+    char *(*get_connection_iface)(NMDeviceFactory *factory,
+                                  NMConnection *   connection,
+                                  const char *     parent_iface);
+
+    /**
+     * create_device:
+     * @factory: the #NMDeviceFactory
+     * @iface: the interface name of the device
+     * @plink: the #NMPlatformLink if backed by a kernel device
+     * @connection: the #NMConnection if not backed by a kernel device
+     * @out_ignore: on return, %TRUE if the link should be ignored
+     *
+     * The plugin should create a new unrealized device using the details given
+     * by @iface and @plink or @connection.  If both @iface and @plink are given,
+     * they are guaranteed to match.  If both @iface and @connection are given,
+     * @iface is guaranteed to be the interface name that @connection specifies.
+     *
+     * If the plugin cannot create a #NMDevice for the link and wants the
+     * core to ignore it, set @out_ignore to %TRUE and return %NULL.
+     *
+     * Returns: the new unrealized #NMDevice, or %NULL
+     */
+    NMDevice *(*create_device)(NMDeviceFactory *     factory,
+                               const char *          iface,
+                               const NMPlatformLink *plink,
+                               NMConnection *        connection,
+                               gboolean *            out_ignore);
+
+} NMDeviceFactoryClass;
+
+GType nm_device_factory_get_type(void);
+
+/*****************************************************************************/
+
+/**
+ * nm_device_factory_create:
+ * @error: an error if creation of the factory failed, or %NULL
+ *
+ * Creates a #GObject that implements the #NMDeviceFactory interface. This
+ * function must not emit any signals or perform any actions that would cause
+ * devices or components to be created immediately.  Instead these should be
+ * deferred to the "start" interface method.
+ *
+ * Returns: the #GObject implementing #NMDeviceFactory or %NULL
+ */
+NMDeviceFactory *nm_device_factory_create(GError **error);
+
+/* Should match nm_device_factory_create() */
+typedef NMDeviceFactory *(*NMDeviceFactoryCreateFunc)(GError **error);
+
+/*****************************************************************************/
+
+const char *nm_device_factory_get_connection_parent(NMDeviceFactory *factory,
+                                                    NMConnection *   connection);
+
+char *nm_device_factory_get_connection_iface(NMDeviceFactory *factory,
+                                             NMConnection *   connection,
+                                             const char *     parent_iface,
+                                             GError **        error);
+
+void nm_device_factory_start(NMDeviceFactory *factory);
+
+NMDevice *nm_device_factory_create_device(NMDeviceFactory *     factory,
+                                          const char *          iface,
+                                          const NMPlatformLink *plink,
+                                          NMConnection *        connection,
+                                          gboolean *            out_ignore,
+                                          GError **             error);
+
+#define NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(...)                                          \
+    {                                                                                      \
+        static NMLinkType const _link_types_declared[] = {__VA_ARGS__, NM_LINK_TYPE_NONE}; \
+        _link_types                                    = _link_types_declared;             \
+    }
+#define NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(...)                                  \
+    {                                                                                 \
+        static const char *const _setting_types_declared[] = {__VA_ARGS__, NULL};     \
+        _setting_types                                     = _setting_types_declared; \
+    }
+
+#define NM_DEVICE_FACTORY_DECLARE_TYPES(...)                                   \
+    static void get_supported_types(NMDeviceFactory *   factory,               \
+                                    const NMLinkType ** out_link_types,        \
+                                    const char *const **out_setting_types)     \
+    {                                                                          \
+        static NMLinkType const  _link_types_null[1]    = {NM_LINK_TYPE_NONE}; \
+        static const char *const _setting_types_null[1] = {NULL};              \
+                                                                               \
+        const NMLinkType * _link_types    = _link_types_null;                  \
+        const char *const *_setting_types = _setting_types_null;               \
+                                                                               \
+        {                                                                      \
+            __VA_ARGS__;                                                       \
+        }                                                                      \
+                                                                               \
+        NM_SET_OUT(out_link_types, _link_types);                               \
+        NM_SET_OUT(out_setting_types, _setting_types);                         \
+    }
+
+/**************************************************************************
+ * INTERNAL DEVICE FACTORY FUNCTIONS - devices provided by plugins should
+ * not use these functions.
+ **************************************************************************/
+
+#define NM_DEVICE_FACTORY_DEFINE_INTERNAL(upper, mixed, lower, st_code, dfi_code)                \
+    typedef struct {                                                                             \
+        NMDeviceFactory parent;                                                                  \
+    } NM##mixed##DeviceFactory;                                                                  \
+    typedef struct {                                                                             \
+        NMDeviceFactoryClass parent;                                                             \
+    } NM##mixed##DeviceFactoryClass;                                                             \
+                                                                                                 \
+    GType nm_##lower##_device_factory_get_type(void);                                            \
+                                                                                                 \
+    G_DEFINE_TYPE(NM##mixed##DeviceFactory, nm_##lower##_device_factory, NM_TYPE_DEVICE_FACTORY) \
+                                                                                                 \
+    NM_DEVICE_FACTORY_DECLARE_TYPES(st_code)                                                     \
+                                                                                                 \
+    static void nm_##lower##_device_factory_init(NM##mixed##DeviceFactory *self) {}              \
+                                                                                                 \
+    static void nm_##lower##_device_factory_class_init(NM##mixed##DeviceFactoryClass *klass)     \
+    {                                                                                            \
+        NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);                    \
+                                                                                                 \
+        factory_class->get_supported_types = get_supported_types;                                \
+        dfi_code                                                                                 \
+    }
+
+/**************************************************************************
+ * PRIVATE FACTORY FUNCTIONS - for factory consumers (eg, NMManager).
+ **************************************************************************/
+
+typedef void (*NMDeviceFactoryManagerFactoryFunc)(NMDeviceFactory *factory, gpointer user_data);
+
+void nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callback,
+                                              gpointer                          user_data);
+
+NMDeviceFactory *nm_device_factory_manager_find_factory_for_link_type(NMLinkType link_type);
+
+NMDeviceFactory *nm_device_factory_manager_find_factory_for_connection(NMConnection *connection);
+
+void nm_device_factory_manager_for_each_factory(NMDeviceFactoryManagerFactoryFunc callback,
+                                                gpointer                          user_data);
+
+#endif /* __NETWORKMANAGER_DEVICE_FACTORY_H__ */
diff --git a/src/core/devices/nm-device-generic.c b/src/core/devices/nm-device-generic.c
new file mode 100644
index 00000000..a319666a
--- /dev/null
+++ b/src/core/devices/nm-device-generic.c
@@ -0,0 +1,238 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-generic.h"
+
+#include "nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-core-internal.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_TYPE_DESCRIPTION, );
+
+typedef struct {
+    char *type_description;
+} NMDeviceGenericPrivate;
+
+struct _NMDeviceGeneric {
+    NMDevice               parent;
+    NMDeviceGenericPrivate _priv;
+};
+
+struct _NMDeviceGenericClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceGeneric, nm_device_generic, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_GENERIC_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceGeneric, NM_IS_DEVICE_GENERIC, NMDevice)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    int ifindex = nm_device_get_ifindex(device);
+
+    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;
+}
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    if (NM_DEVICE_GENERIC_GET_PRIVATE(device)->type_description)
+        return NM_DEVICE_GENERIC_GET_PRIVATE(device)->type_description;
+    return NM_DEVICE_CLASS(nm_device_generic_parent_class)->get_type_description(device);
+}
+
+static void
+realize_start_notify(NMDevice *device, const NMPlatformLink *plink)
+{
+    NMDeviceGeneric *       self = NM_DEVICE_GENERIC(device);
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+    int                     ifindex;
+
+    NM_DEVICE_CLASS(nm_device_generic_parent_class)->realize_start_notify(device, plink);
+
+    nm_clear_g_free(&priv->type_description);
+    ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+    if (ifindex > 0)
+        priv->type_description =
+            g_strdup(nm_platform_link_get_type_name(nm_device_get_platform(device), ifindex));
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingConnection *s_con;
+
+    if (!NM_DEVICE_CLASS(nm_device_generic_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    if (!nm_setting_connection_get_interface_name(s_con)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "generic profiles need an interface name");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMSettingConnection *s_con;
+
+    if (!nm_connection_get_setting_generic(connection))
+        nm_connection_add_setting(connection, nm_setting_generic_new());
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    g_object_set(G_OBJECT(s_con),
+                 NM_SETTING_CONNECTION_INTERFACE_NAME,
+                 nm_device_get_iface(device),
+                 NULL);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceGeneric *       self = NM_DEVICE_GENERIC(object);
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_TYPE_DESCRIPTION:
+        g_value_set_string(value, priv->type_description);
+        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 = NM_DEVICE_GENERIC(object);
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_TYPE_DESCRIPTION:
+        priv->type_description = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_generic_init(NMDeviceGeneric *self)
+{}
+
+static GObject *
+constructor(GType type, guint n_construct_params, GObjectConstructParam *construct_params)
+{
+    GObject *object;
+
+    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);
+
+    return object;
+}
+
+NMDevice *
+nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing)
+{
+    g_return_val_if_fail(plink != NULL, NULL);
+
+    return g_object_new(NM_TYPE_DEVICE_GENERIC,
+                        NM_DEVICE_IFACE,
+                        plink->name,
+                        NM_DEVICE_TYPE_DESC,
+                        "Generic",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_GENERIC,
+                        NM_DEVICE_NM_PLUGIN_MISSING,
+                        nm_plugin_missing,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceGeneric *       self = NM_DEVICE_GENERIC(object);
+    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
+
+    nm_clear_g_free(&priv->type_description);
+
+    G_OBJECT_CLASS(nm_device_generic_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_generic = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_GENERIC,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L(
+                "TypeDescription",
+                "s",
+                NM_DEVICE_GENERIC_TYPE_DESCRIPTION), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_generic_class_init(NMDeviceGenericClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructor  = constructor;
+    object_class->dispose      = dispose;
+    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);
+
+    device_class->connection_type_supported        = NM_SETTING_GENERIC_SETTING_NAME;
+    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->get_generic_capabilities    = get_generic_capabilities;
+    device_class->get_type_description        = get_type_description;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->update_connection           = update_connection;
+
+    obj_properties[PROP_TYPE_DESCRIPTION] =
+        g_param_spec_string(NM_DEVICE_GENERIC_TYPE_DESCRIPTION,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/nm-device-generic.h b/src/core/devices/nm-device-generic.h
new file mode 100644
index 00000000..48c43523
--- /dev/null
+++ b/src/core/devices/nm-device-generic.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_GENERIC_H__
+#define __NETWORKMANAGER_DEVICE_GENERIC_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_GENERIC (nm_device_generic_get_type())
+#define NM_DEVICE_GENERIC(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_GENERIC, NMDeviceGeneric))
+#define NM_DEVICE_GENERIC_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_GENERIC, NMDeviceGenericClass))
+#define NM_IS_DEVICE_GENERIC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_GENERIC))
+#define NM_IS_DEVICE_GENERIC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_GENERIC))
+#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"
+
+typedef struct _NMDeviceGeneric      NMDeviceGeneric;
+typedef struct _NMDeviceGenericClass NMDeviceGenericClass;
+
+GType nm_device_generic_get_type(void);
+
+NMDevice *nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing);
+
+#endif /* __NETWORKMANAGER_DEVICE_GENERIC_H__ */
diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c
new file mode 100644
index 00000000..f54ffcf0
--- /dev/null
+++ b/src/core/devices/nm-device-infiniband.c
@@ -0,0 +1,504 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-infiniband.h"
+
+#include <linux/if.h>
+#include <linux/if_infiniband.h>
+
+#include "NetworkManagerUtils.h"
+#include "nm-device-private.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-core-internal.h"
+
+#define NM_DEVICE_INFINIBAND_IS_PARTITION "is-partition"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_IS_PARTITION, );
+
+typedef struct {
+    gboolean is_partition;
+    int      parent_ifindex;
+    int      p_key;
+} NMDeviceInfinibandPrivate;
+
+struct _NMDeviceInfiniband {
+    NMDevice                  parent;
+    NMDeviceInfinibandPrivate _priv;
+};
+
+struct _NMDeviceInfinibandClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceInfiniband, nm_device_infiniband, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_INFINIBAND_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceInfiniband, NM_IS_DEVICE_INFINIBAND, NMDevice)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    guint32 caps = NM_DEVICE_CAP_CARRIER_DETECT;
+
+    if (NM_DEVICE_INFINIBAND_GET_PRIVATE(device)->is_partition)
+        caps |= NM_DEVICE_CAP_IS_SOFTWARE;
+
+    return caps;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    nm_auto_close int    dirfd = -1;
+    NMSettingInfiniband *s_infiniband;
+    char                 ifname_verified[IFNAMSIZ];
+    const char *         transport_mode;
+    gboolean             ok;
+
+    s_infiniband = nm_device_get_applied_setting(device, NM_TYPE_SETTING_INFINIBAND);
+
+    g_return_val_if_fail(s_infiniband, NM_ACT_STAGE_RETURN_FAILURE);
+
+    transport_mode = nm_setting_infiniband_get_transport_mode(s_infiniband);
+
+    dirfd = nm_platform_sysctl_open_netdir(nm_device_get_platform(device),
+                                           nm_device_get_ifindex(device),
+                                           ifname_verified);
+    if (dirfd < 0) {
+        if (nm_streq(transport_mode, "datagram"))
+            return NM_ACT_STAGE_RETURN_SUCCESS;
+        else {
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_INFINIBAND_MODE);
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+    }
+
+    /* With some drivers the interface must be down to set transport mode */
+    nm_device_take_down(device, TRUE);
+    ok = nm_platform_sysctl_set(nm_device_get_platform(device),
+                                NMP_SYSCTL_PATHID_NETDIR(dirfd, ifname_verified, "mode"),
+                                transport_mode);
+    nm_device_bring_up(device, TRUE, NULL);
+
+    if (!ok) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_INFINIBAND,
+                                                        out_source);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingInfiniband *s_infiniband;
+
+    if (!NM_DEVICE_CLASS(nm_device_infiniband_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_device_is_real(device)) {
+        const char *mac;
+        const char *hw_addr;
+
+        s_infiniband = nm_connection_get_setting_infiniband(connection);
+
+        mac = nm_setting_infiniband_get_mac_address(s_infiniband);
+        if (mac) {
+            hw_addr = nm_device_get_permanent_hw_address(device);
+            if (!hw_addr || !nm_utils_hwaddr_matches(mac, -1, hw_addr, -1)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "MAC address mismatches");
+                return FALSE;
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingInfiniband *s_infiniband;
+
+    s_infiniband = nm_connection_get_setting_infiniband(connection);
+    if (!s_infiniband) {
+        s_infiniband = (NMSettingInfiniband *) nm_setting_infiniband_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_infiniband));
+    }
+
+    nm_utils_complete_generic(
+        nm_device_get_platform(device),
+        connection,
+        NM_SETTING_INFINIBAND_SETTING_NAME,
+        existing_connections,
+        NULL,
+        _("InfiniBand connection"),
+        NULL,
+        nm_setting_infiniband_get_mac_address(s_infiniband) ? NULL : nm_device_get_iface(device),
+        TRUE);
+
+    if (!nm_setting_infiniband_get_transport_mode(s_infiniband))
+        g_object_set(G_OBJECT(s_infiniband),
+                     NM_SETTING_INFINIBAND_TRANSPORT_MODE,
+                     "datagram",
+                     NULL);
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMSettingInfiniband *s_infiniband   = nm_connection_get_setting_infiniband(connection);
+    const char *         mac            = nm_device_get_permanent_hw_address(device);
+    const char *         transport_mode = "datagram";
+    int                  ifindex;
+
+    if (!s_infiniband) {
+        s_infiniband = (NMSettingInfiniband *) nm_setting_infiniband_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_infiniband);
+    }
+
+    if (mac && !nm_utils_hwaddr_matches(mac, -1, NULL, INFINIBAND_ALEN))
+        g_object_set(s_infiniband, NM_SETTING_INFINIBAND_MAC_ADDRESS, mac, NULL);
+
+    ifindex = nm_device_get_ifindex(device);
+    if (ifindex > 0) {
+        if (!nm_platform_link_infiniband_get_properties(nm_device_get_platform(device),
+                                                        ifindex,
+                                                        NULL,
+                                                        NULL,
+                                                        &transport_mode))
+            transport_mode = "datagram";
+    }
+    g_object_set(G_OBJECT(s_infiniband),
+                 NM_SETTING_INFINIBAND_TRANSPORT_MODE,
+                 transport_mode,
+                 NULL);
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    if (nm_streq(setting_name, NM_SETTING_INFINIBAND_SETTING_NAME)) {
+        return nm_device_hash_check_invalid_keys(
+            diffs,
+            NM_SETTING_INFINIBAND_SETTING_NAME,
+            error,
+            NM_SETTING_INFINIBAND_MTU); /* reapplied with IP config */
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_infiniband_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    NMDeviceInfinibandPrivate *priv = NM_DEVICE_INFINIBAND_GET_PRIVATE(device);
+    NMSettingInfiniband *      s_infiniband;
+    int                        r;
+
+    s_infiniband = nm_connection_get_setting_infiniband(connection);
+    g_assert(s_infiniband);
+
+    /* Can only create partitions at this time */
+    priv->p_key = nm_setting_infiniband_get_p_key(s_infiniband);
+    if (priv->p_key < 0) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_FAILED,
+                            "only InfiniBand partitions can be created");
+        return FALSE;
+    }
+
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "InfiniBand partitions can not be created without a parent interface");
+        return FALSE;
+    }
+
+    if (!NM_IS_DEVICE_INFINIBAND(parent)) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "Parent interface %s must be an InfiniBand interface",
+                    nm_device_get_iface(parent));
+        return FALSE;
+    }
+
+    priv->parent_ifindex = nm_device_get_ifindex(parent);
+    if (priv->parent_ifindex <= 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "failed to get InfiniBand parent %s ifindex",
+                    nm_device_get_iface(parent));
+        return FALSE;
+    }
+
+    r = nm_platform_link_infiniband_add(nm_device_get_platform(device),
+                                        priv->parent_ifindex,
+                                        priv->p_key,
+                                        out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create InfiniBand P_Key interface '%s' for '%s': %s",
+                    nm_device_get_iface(device),
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    priv->is_partition = TRUE;
+    return TRUE;
+}
+
+static gboolean
+unrealize(NMDevice *device, GError **error)
+{
+    NMDeviceInfinibandPrivate *priv;
+    int                        r;
+
+    g_return_val_if_fail(NM_IS_DEVICE_INFINIBAND(device), FALSE);
+
+    priv = NM_DEVICE_INFINIBAND_GET_PRIVATE(device);
+
+    if (priv->p_key < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "Only InfiniBand partitions can be removed");
+        return FALSE;
+    }
+
+    r = nm_platform_link_infiniband_delete(nm_device_get_platform(device),
+                                           priv->parent_ifindex,
+                                           priv->p_key);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to remove InfiniBand P_Key interface '%s': %s",
+                    nm_device_get_iface(device),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    switch (prop_id) {
+    case PROP_IS_PARTITION:
+        g_value_set_boolean(value, NM_DEVICE_INFINIBAND_GET_PRIVATE(object)->is_partition);
+        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)
+{
+    switch (prop_id) {
+    case PROP_IS_PARTITION:
+        NM_DEVICE_INFINIBAND_GET_PRIVATE(object)->is_partition = g_value_get_boolean(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_infiniband_init(NMDeviceInfiniband *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_infiniband = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_INFINIBAND,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier",
+                                                             "b",
+                                                             NM_DEVICE_CARRIER), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_infiniband_class_init(NMDeviceInfinibandClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_infiniband);
+
+    device_class->connection_type_supported        = NM_SETTING_INFINIBAND_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_INFINIBAND_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_INFINIBAND);
+
+    device_class->can_reapply_change          = can_reapply_change;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->unrealize                   = unrealize;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+    device_class->update_connection           = update_connection;
+
+    device_class->act_stage1_prepare = act_stage1_prepare;
+    device_class->get_configured_mtu = get_configured_mtu;
+
+    obj_properties[PROP_IS_PARTITION] =
+        g_param_spec_boolean(NM_DEVICE_INFINIBAND_IS_PARTITION,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_INFINIBAND_DEVICE_FACTORY (nm_infiniband_device_factory_get_type())
+#define NM_INFINIBAND_DEVICE_FACTORY(obj)                          \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj),                             \
+                                NM_TYPE_INFINIBAND_DEVICE_FACTORY, \
+                                NMInfinibandDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    gboolean is_partition = FALSE;
+
+    if (plink)
+        is_partition = (plink->parent > 0 || plink->parent == NM_PLATFORM_LINK_OTHER_NETNS);
+    else if (connection) {
+        NMSettingInfiniband *s_infiniband;
+
+        s_infiniband = nm_connection_get_setting_infiniband(connection);
+        g_return_val_if_fail(s_infiniband, NULL);
+        is_partition = !!nm_setting_infiniband_get_parent(s_infiniband)
+                       || (nm_setting_infiniband_get_p_key(s_infiniband) >= 0
+                           && nm_setting_infiniband_get_mac_address(s_infiniband));
+    }
+
+    return g_object_new(NM_TYPE_DEVICE_INFINIBAND,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "InfiniBand",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_INFINIBAND,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_INFINIBAND,
+                        /* NOTE: Partition should probably be a different link type! */
+                        NM_DEVICE_INFINIBAND_IS_PARTITION,
+                        is_partition,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingInfiniband *s_infiniband;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_INFINIBAND_SETTING_NAME),
+                         NULL);
+
+    s_infiniband = nm_connection_get_setting_infiniband(connection);
+    g_assert(s_infiniband);
+
+    return nm_setting_infiniband_get_parent(s_infiniband);
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    NMSettingInfiniband *s_infiniband;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_INFINIBAND_SETTING_NAME),
+                         NULL);
+
+    s_infiniband = nm_connection_get_setting_infiniband(connection);
+    g_assert(s_infiniband);
+
+    if (!parent_iface)
+        return NULL;
+
+    g_return_val_if_fail(g_strcmp0(parent_iface, nm_setting_infiniband_get_parent(s_infiniband))
+                             == 0,
+                         NULL);
+
+    return g_strdup(nm_setting_infiniband_get_virtual_interface_name(s_infiniband));
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    INFINIBAND,
+    Infiniband,
+    infiniband,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_INFINIBAND)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_INFINIBAND_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-infiniband.h b/src/core/devices/nm-device-infiniband.h
new file mode 100644
index 00000000..69994a2d
--- /dev/null
+++ b/src/core/devices/nm-device-infiniband.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_INFINIBAND_H__
+#define __NETWORKMANAGER_DEVICE_INFINIBAND_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_INFINIBAND (nm_device_infiniband_get_type())
+#define NM_DEVICE_INFINIBAND(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfiniband))
+#define NM_DEVICE_INFINIBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfinibandClass))
+#define NM_IS_DEVICE_INFINIBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_INFINIBAND))
+#define NM_IS_DEVICE_INFINIBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_INFINIBAND))
+#define NM_DEVICE_INFINIBAND_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_INFINIBAND, NMDeviceInfinibandClass))
+
+typedef struct _NMDeviceInfiniband      NMDeviceInfiniband;
+typedef struct _NMDeviceInfinibandClass NMDeviceInfinibandClass;
+
+GType nm_device_infiniband_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_INFINIBAND_H__ */
diff --git a/src/core/devices/nm-device-ip-tunnel.c b/src/core/devices/nm-device-ip-tunnel.c
new file mode 100644
index 00000000..da6afb3b
--- /dev/null
+++ b/src/core/devices/nm-device-ip-tunnel.c
@@ -0,0 +1,1299 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ip-tunnel.h"
+
+#include <netinet/in.h>
+#include <linux/if.h>
+#include <linux/ip.h>
+#include <linux/if_tunnel.h>
+#include <linux/ip6_tunnel.h>
+#include <linux/if_ether.h>
+
+#include "nm-device-private.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-core-internal.h"
+#include "settings/nm-settings.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceIPTunnel
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIPTunnel,
+                             PROP_MODE,
+                             PROP_LOCAL,
+                             PROP_REMOTE,
+                             PROP_TTL,
+                             PROP_TOS,
+                             PROP_PATH_MTU_DISCOVERY,
+                             PROP_INPUT_KEY,
+                             PROP_OUTPUT_KEY,
+                             PROP_ENCAPSULATION_LIMIT,
+                             PROP_FLOW_LABEL,
+                             PROP_FLAGS, );
+
+typedef struct {
+    NMIPTunnelMode  mode;
+    char *          local;
+    char *          remote;
+    guint8          ttl;
+    guint8          tos;
+    gboolean        path_mtu_discovery;
+    int             addr_family;
+    char *          input_key;
+    char *          output_key;
+    guint8          encap_limit;
+    guint32         flow_label;
+    NMIPTunnelFlags flags;
+} NMDeviceIPTunnelPrivate;
+
+struct _NMDeviceIPTunnel {
+    NMDevice                parent;
+    NMDeviceIPTunnelPrivate _priv;
+};
+
+struct _NMDeviceIPTunnelClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceIPTunnel, nm_device_ip_tunnel, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceIPTunnel, NM_IS_DEVICE_IP_TUNNEL, NMDevice)
+
+/*****************************************************************************/
+
+static guint32
+ip6tnl_flags_setting_to_plat(NMIPTunnelFlags flags)
+{
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_IGN_ENCAP_LIMIT == IP6_TNL_F_IGN_ENCAP_LIMIT);
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_TCLASS == IP6_TNL_F_USE_ORIG_TCLASS);
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_FLOWLABEL == IP6_TNL_F_USE_ORIG_FLOWLABEL);
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_MIP6_DEV == IP6_TNL_F_MIP6_DEV);
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_RCV_DSCP_COPY == IP6_TNL_F_RCV_DSCP_COPY);
+    G_STATIC_ASSERT(NM_IP_TUNNEL_FLAG_IP6_USE_ORIG_FWMARK == IP6_TNL_F_USE_ORIG_FWMARK);
+
+    /* NOTE: "accidentally", the numeric values correspond.
+     *       For flags added in the future, that might no longer
+     *       be the case. */
+    return flags & _NM_IP_TUNNEL_FLAG_ALL_IP6TNL;
+}
+
+static NMIPTunnelFlags
+ip6tnl_flags_plat_to_setting(guint32 flags)
+{
+    return flags & ((guint32) _NM_IP_TUNNEL_FLAG_ALL_IP6TNL);
+}
+
+/*****************************************************************************/
+
+static gboolean
+address_equal_pp(int addr_family, const char *a, const char *b)
+{
+    const NMIPAddr *addr_a = &nm_ip_addr_zero;
+    const NMIPAddr *addr_b = &nm_ip_addr_zero;
+    NMIPAddr        addr_a_val;
+    NMIPAddr        addr_b_val;
+
+    nm_assert_addr_family(addr_family);
+
+    if (a) {
+        if (!nm_utils_parse_inaddr_bin(addr_family, a, NULL, &addr_a_val))
+            nm_assert_not_reached();
+        addr_a = &addr_a_val;
+    }
+    if (b) {
+        if (!nm_utils_parse_inaddr_bin(addr_family, b, NULL, &addr_b_val))
+            nm_assert_not_reached();
+        addr_b = &addr_b_val;
+    }
+
+    return nm_ip_addr_equal(addr_family, addr_a, addr_b);
+}
+
+static gboolean
+address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
+{
+    nm_assert_addr_family(addr_family);
+    nm_assert(p_addr);
+    nm_assert(!*p_addr || nm_utils_ipaddr_is_normalized(addr_family, *p_addr));
+
+    if (!addr_new || nm_ip_addr_is_null(addr_family, addr_new)) {
+        if (nm_clear_g_free(p_addr))
+            return TRUE;
+        return FALSE;
+    }
+
+    if (*p_addr) {
+        NMIPAddr addr_val;
+
+        if (!nm_utils_parse_inaddr_bin(addr_family, *p_addr, NULL, &addr_val))
+            nm_assert_not_reached();
+
+        if (nm_ip_addr_equal(addr_family, &addr_val, addr_new))
+            return FALSE;
+
+        g_free(*p_addr);
+    }
+
+    *p_addr = nm_utils_inet_ntop_dup(addr_family, addr_new);
+    return TRUE;
+}
+
+static void
+update_properties_from_ifindex(NMDevice *device, int ifindex)
+{
+    NMDeviceIPTunnel *       self           = NM_DEVICE_IP_TUNNEL(device);
+    NMDeviceIPTunnelPrivate *priv           = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
+    int                      parent_ifindex = 0;
+    NMIPAddr                 local          = NM_IP_ADDR_INIT;
+    NMIPAddr                 remote         = NM_IP_ADDR_INIT;
+    guint8                   ttl            = 0;
+    guint8                   tos            = 0;
+    guint8                   encap_limit    = 0;
+    gboolean                 pmtud          = FALSE;
+    guint32                  flow_label     = 0;
+    NMIPTunnelFlags          flags          = NM_IP_TUNNEL_FLAG_NONE;
+    char *                   key;
+
+    if (ifindex <= 0) {
+clear:
+        nm_device_parent_set_ifindex(device, 0);
+        if (priv->local) {
+            nm_clear_g_free(&priv->local);
+            _notify(self, PROP_LOCAL);
+        }
+        if (priv->remote) {
+            nm_clear_g_free(&priv->remote);
+            _notify(self, PROP_REMOTE);
+        }
+        if (priv->input_key) {
+            nm_clear_g_free(&priv->input_key);
+            _notify(self, PROP_INPUT_KEY);
+        }
+        if (priv->output_key) {
+            nm_clear_g_free(&priv->output_key);
+            _notify(self, PROP_OUTPUT_KEY);
+        }
+
+        goto out;
+    }
+
+    if (NM_IN_SET(priv->mode, NM_IP_TUNNEL_MODE_GRE, NM_IP_TUNNEL_MODE_GRETAP)) {
+        const NMPlatformLnkGre *lnk;
+
+        if (priv->mode == NM_IP_TUNNEL_MODE_GRE)
+            lnk = nm_platform_link_get_lnk_gre(nm_device_get_platform(device), ifindex, NULL);
+        else
+            lnk = nm_platform_link_get_lnk_gretap(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "gre");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr4    = lnk->local;
+        remote.addr4   = lnk->remote;
+        ttl            = lnk->ttl;
+        tos            = lnk->tos;
+        pmtud          = lnk->path_mtu_discovery;
+
+        if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) {
+            key = g_strdup_printf("%u", lnk->input_key);
+            if (g_strcmp0(priv->input_key, key)) {
+                g_free(priv->input_key);
+                priv->input_key = key;
+                _notify(self, PROP_INPUT_KEY);
+            } else
+                g_free(key);
+        } else {
+            if (priv->input_key) {
+                nm_clear_g_free(&priv->input_key);
+                _notify(self, PROP_INPUT_KEY);
+            }
+        }
+
+        if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) {
+            key = g_strdup_printf("%u", lnk->output_key);
+            if (g_strcmp0(priv->output_key, key)) {
+                g_free(priv->output_key);
+                priv->output_key = key;
+                _notify(self, PROP_OUTPUT_KEY);
+            } else
+                g_free(key);
+        } else {
+            if (priv->output_key) {
+                nm_clear_g_free(&priv->output_key);
+                _notify(self, PROP_OUTPUT_KEY);
+            }
+        }
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_SIT) {
+        const NMPlatformLnkSit *lnk;
+
+        lnk = nm_platform_link_get_lnk_sit(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "sit");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr4    = lnk->local;
+        remote.addr4   = lnk->remote;
+        ttl            = lnk->ttl;
+        tos            = lnk->tos;
+        pmtud          = lnk->path_mtu_discovery;
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_IPIP) {
+        const NMPlatformLnkIpIp *lnk;
+
+        lnk = nm_platform_link_get_lnk_ipip(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "ipip");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr4    = lnk->local;
+        remote.addr4   = lnk->remote;
+        ttl            = lnk->ttl;
+        tos            = lnk->tos;
+        pmtud          = lnk->path_mtu_discovery;
+    } else if (NM_IN_SET(priv->mode,
+                         NM_IP_TUNNEL_MODE_IPIP6,
+                         NM_IP_TUNNEL_MODE_IP6IP6,
+                         NM_IP_TUNNEL_MODE_IP6GRE,
+                         NM_IP_TUNNEL_MODE_IP6GRETAP)) {
+        const NMPlatformLnkIp6Tnl *lnk;
+        NMPlatform *               plat = nm_device_get_platform(device);
+
+        if (priv->mode == NM_IP_TUNNEL_MODE_IP6GRE)
+            lnk = nm_platform_link_get_lnk_ip6gre(plat, ifindex, NULL);
+        else if (priv->mode == NM_IP_TUNNEL_MODE_IP6GRETAP)
+            lnk = nm_platform_link_get_lnk_ip6gretap(plat, ifindex, NULL);
+        else
+            lnk = nm_platform_link_get_lnk_ip6tnl(plat, ifindex, NULL);
+
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "ip6tnl");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr6    = lnk->local;
+        remote.addr6   = lnk->remote;
+        ttl            = lnk->ttl;
+        tos            = lnk->tclass;
+        encap_limit    = lnk->encap_limit;
+        flow_label     = lnk->flow_label;
+        flags          = ip6tnl_flags_plat_to_setting(lnk->flags);
+
+        if (NM_IN_SET(priv->mode, NM_IP_TUNNEL_MODE_IP6GRE, NM_IP_TUNNEL_MODE_IP6GRETAP)) {
+            if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY)) {
+                key = g_strdup_printf("%u", lnk->input_key);
+                if (g_strcmp0(priv->input_key, key)) {
+                    g_free(priv->input_key);
+                    priv->input_key = key;
+                    _notify(self, PROP_INPUT_KEY);
+                } else
+                    g_free(key);
+            } else {
+                if (priv->input_key) {
+                    nm_clear_g_free(&priv->input_key);
+                    _notify(self, PROP_INPUT_KEY);
+                }
+            }
+
+            if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY)) {
+                key = g_strdup_printf("%u", lnk->output_key);
+                if (g_strcmp0(priv->output_key, key)) {
+                    g_free(priv->output_key);
+                    priv->output_key = key;
+                    _notify(self, PROP_OUTPUT_KEY);
+                } else
+                    g_free(key);
+            } else {
+                if (priv->output_key) {
+                    nm_clear_g_free(&priv->output_key);
+                    _notify(self, PROP_OUTPUT_KEY);
+                }
+            }
+        }
+    } else
+        g_return_if_reached();
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+
+    if (address_set(priv->addr_family, &priv->local, &local))
+        _notify(self, PROP_LOCAL);
+    if (address_set(priv->addr_family, &priv->remote, &remote))
+        _notify(self, PROP_REMOTE);
+
+out:
+
+    if (priv->ttl != ttl) {
+        priv->ttl = ttl;
+        _notify(self, PROP_TTL);
+    }
+
+    if (priv->tos != tos) {
+        priv->tos = tos;
+        _notify(self, PROP_TOS);
+    }
+
+    if (priv->path_mtu_discovery != pmtud) {
+        priv->path_mtu_discovery = pmtud;
+        _notify(self, PROP_PATH_MTU_DISCOVERY);
+    }
+
+    if (priv->encap_limit != encap_limit) {
+        priv->encap_limit = encap_limit;
+        _notify(self, PROP_ENCAPSULATION_LIMIT);
+    }
+
+    if (priv->flow_label != flow_label) {
+        priv->flow_label = flow_label;
+        _notify(self, PROP_FLOW_LABEL);
+    }
+
+    if (priv->flags != flags) {
+        priv->flags = flags;
+        _notify(self, PROP_FLAGS);
+    }
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    update_properties_from_ifindex(device, nm_device_get_ifindex(device));
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingIPTunnel *s_ip_tunnel;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_IP_TUNNEL_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("IP tunnel connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    if (!s_ip_tunnel) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'tunnel' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceIPTunnel *       self        = NM_DEVICE_IP_TUNNEL(device);
+    NMDeviceIPTunnelPrivate *priv        = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
+    NMSettingIPTunnel *      s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+
+    if (!s_ip_tunnel) {
+        s_ip_tunnel = (NMSettingIPTunnel *) nm_setting_ip_tunnel_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_ip_tunnel);
+    }
+
+    if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode)
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_MODE, priv->mode, NULL);
+
+    g_object_set(
+        s_ip_tunnel,
+        NM_SETTING_IP_TUNNEL_PARENT,
+        nm_device_parent_find_for_connection(device, nm_setting_ip_tunnel_get_parent(s_ip_tunnel)),
+        NULL);
+
+    if (!address_equal_pp(priv->addr_family,
+                          nm_setting_ip_tunnel_get_local(s_ip_tunnel),
+                          priv->local))
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_LOCAL, priv->local, NULL);
+
+    if (!address_equal_pp(priv->addr_family,
+                          nm_setting_ip_tunnel_get_remote(s_ip_tunnel),
+                          priv->remote))
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_REMOTE, priv->remote, NULL);
+
+    if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl)
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_TTL, priv->ttl, NULL);
+
+    if (nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos)
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_TOS, priv->tos, NULL);
+
+    if (nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel) != priv->path_mtu_discovery) {
+        g_object_set(G_OBJECT(s_ip_tunnel),
+                     NM_SETTING_IP_TUNNEL_PATH_MTU_DISCOVERY,
+                     priv->path_mtu_discovery,
+                     NULL);
+    }
+
+    if (nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel) != priv->encap_limit) {
+        g_object_set(G_OBJECT(s_ip_tunnel),
+                     NM_SETTING_IP_TUNNEL_ENCAPSULATION_LIMIT,
+                     priv->encap_limit,
+                     NULL);
+    }
+
+    if (nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel) != priv->flow_label) {
+        g_object_set(G_OBJECT(s_ip_tunnel),
+                     NM_SETTING_IP_TUNNEL_FLOW_LABEL,
+                     priv->flow_label,
+                     NULL);
+    }
+
+    if (NM_IN_SET(priv->mode,
+                  NM_IP_TUNNEL_MODE_GRE,
+                  NM_IP_TUNNEL_MODE_GRETAP,
+                  NM_IP_TUNNEL_MODE_IP6GRE,
+                  NM_IP_TUNNEL_MODE_IP6GRETAP)) {
+        if (g_strcmp0(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel), priv->input_key)) {
+            g_object_set(G_OBJECT(s_ip_tunnel),
+                         NM_SETTING_IP_TUNNEL_INPUT_KEY,
+                         priv->input_key,
+                         NULL);
+        }
+        if (g_strcmp0(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel), priv->output_key)) {
+            g_object_set(G_OBJECT(s_ip_tunnel),
+                         NM_SETTING_IP_TUNNEL_OUTPUT_KEY,
+                         priv->output_key,
+                         NULL);
+        }
+    }
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceIPTunnel *       self = NM_DEVICE_IP_TUNNEL(device);
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
+    NMSettingIPTunnel *      s_ip_tunnel;
+    const char *             parent;
+
+    if (!NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+
+    if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "incompatible IP tunnel mode");
+        return FALSE;
+    }
+
+    if (nm_device_is_real(device)) {
+        /* Check parent interface; could be an interface name or a UUID */
+        parent = nm_setting_ip_tunnel_get_parent(s_ip_tunnel);
+        if (parent && !nm_device_match_parent(device, parent)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "IP tunnel parent mismatches");
+            return FALSE;
+        }
+
+        if (!address_equal_pp(priv->addr_family,
+                              nm_setting_ip_tunnel_get_local(s_ip_tunnel),
+                              priv->local)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "local IP tunnel address mismatches");
+            return FALSE;
+        }
+
+        if (!address_equal_pp(priv->addr_family,
+                              nm_setting_ip_tunnel_get_remote(s_ip_tunnel),
+                              priv->remote)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "remote IP tunnel address mismatches");
+            return FALSE;
+        }
+
+        if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "TTL of IP tunnel mismatches");
+            return FALSE;
+        }
+
+        if (nm_setting_ip_tunnel_get_tos(s_ip_tunnel) != priv->tos) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "TOS of IP tunnel mismatches");
+            return FALSE;
+        }
+
+        if (priv->addr_family == AF_INET) {
+            if (nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel)
+                != priv->path_mtu_discovery) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "MTU discovery setting of IP tunnel mismatches");
+                return FALSE;
+            }
+        } else {
+            if (nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel) != priv->encap_limit) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "encapsulation limit of IP tunnel mismatches");
+                return FALSE;
+            }
+
+            if (nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel) != priv->flow_label) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "flow-label of IP tunnel mismatches");
+                return FALSE;
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static NMIPTunnelMode
+platform_link_to_tunnel_mode(const NMPlatformLink *link)
+{
+    const NMPlatformLnkIp6Tnl *lnk;
+
+    switch (link->type) {
+    case NM_LINK_TYPE_GRE:
+        return NM_IP_TUNNEL_MODE_GRE;
+    case NM_LINK_TYPE_GRETAP:
+        return NM_IP_TUNNEL_MODE_GRETAP;
+    case NM_LINK_TYPE_IP6TNL:
+        lnk = nm_platform_link_get_lnk_ip6tnl(NM_PLATFORM_GET, link->ifindex, NULL);
+        if (lnk) {
+            if (lnk->proto == IPPROTO_IPIP)
+                return NM_IP_TUNNEL_MODE_IPIP6;
+            if (lnk->proto == IPPROTO_IPV6)
+                return NM_IP_TUNNEL_MODE_IP6IP6;
+        }
+        return NM_IP_TUNNEL_MODE_UNKNOWN;
+    case NM_LINK_TYPE_IP6GRE:
+        return NM_IP_TUNNEL_MODE_IP6GRE;
+    case NM_LINK_TYPE_IP6GRETAP:
+        return NM_IP_TUNNEL_MODE_IP6GRETAP;
+    case NM_LINK_TYPE_IPIP:
+        return NM_IP_TUNNEL_MODE_IPIP;
+    case NM_LINK_TYPE_SIT:
+        return NM_IP_TUNNEL_MODE_SIT;
+    default:
+        g_return_val_if_reached(NM_IP_TUNNEL_MODE_UNKNOWN);
+    }
+}
+
+static NMLinkType
+tunnel_mode_to_link_type(NMIPTunnelMode tunnel_mode)
+{
+    switch (tunnel_mode) {
+    case NM_IP_TUNNEL_MODE_GRE:
+        return NM_LINK_TYPE_GRE;
+    case NM_IP_TUNNEL_MODE_GRETAP:
+        return NM_LINK_TYPE_GRETAP;
+    case NM_IP_TUNNEL_MODE_IPIP6:
+    case NM_IP_TUNNEL_MODE_IP6IP6:
+        return NM_LINK_TYPE_IP6TNL;
+    case NM_IP_TUNNEL_MODE_IP6GRE:
+        return NM_LINK_TYPE_IP6GRE;
+    case NM_IP_TUNNEL_MODE_IP6GRETAP:
+        return NM_LINK_TYPE_IP6GRETAP;
+    case NM_IP_TUNNEL_MODE_IPIP:
+        return NM_LINK_TYPE_IPIP;
+    case NM_IP_TUNNEL_MODE_SIT:
+        return NM_LINK_TYPE_SIT;
+    case NM_IP_TUNNEL_MODE_VTI:
+    case NM_IP_TUNNEL_MODE_VTI6:
+    case NM_IP_TUNNEL_MODE_ISATAP:
+        return NM_LINK_TYPE_UNKNOWN;
+    case NM_IP_TUNNEL_MODE_UNKNOWN:
+        break;
+    }
+    g_return_val_if_reached(NM_LINK_TYPE_UNKNOWN);
+}
+
+/*****************************************************************************/
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *        iface = nm_device_get_iface(device);
+    NMSettingIPTunnel * s_ip_tunnel;
+    NMPlatformLnkGre    lnk_gre    = {};
+    NMPlatformLnkSit    lnk_sit    = {};
+    NMPlatformLnkIpIp   lnk_ipip   = {};
+    NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
+    const char *        str;
+    gint64              val;
+    NMIPTunnelMode      mode;
+    int                 r;
+    gs_free char *      hwaddr = NULL;
+    guint8              mac_address[ETH_ALEN];
+    gboolean            mac_address_valid = FALSE;
+
+    s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    nm_assert(NM_IS_SETTING_IP_TUNNEL(s_ip_tunnel));
+
+    mode = nm_setting_ip_tunnel_get_mode(s_ip_tunnel);
+
+    if (_nm_ip_tunnel_mode_is_layer2(mode)
+        && nm_device_hw_addr_get_cloned(device, connection, FALSE, &hwaddr, NULL, NULL) && hwaddr) {
+        /* FIXME: we set the MAC address when creating the interface, while the
+         * NMDevice is still unrealized. As we afterwards realize the device, it
+         * forgets the parameters for the cloned MAC address, and in stage 1
+         * it might create a different MAC address. That should be fixed by
+         * better handling device realization. */
+        if (!nm_utils_hwaddr_aton(hwaddr, mac_address, ETH_ALEN)) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "Invalid hardware address '%s'",
+                        hwaddr);
+            g_return_val_if_reached(FALSE);
+        }
+
+        mac_address_valid = TRUE;
+    }
+
+    switch (mode) {
+    case NM_IP_TUNNEL_MODE_GRETAP:
+        lnk_gre.is_tap = TRUE;
+        /* fall-through */
+    case NM_IP_TUNNEL_MODE_GRE:
+        if (parent)
+            lnk_gre.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET, str, &lnk_gre.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        g_assert(str);
+        inet_pton(AF_INET, str, &lnk_gre.remote);
+
+        lnk_gre.ttl                = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel);
+        lnk_gre.tos                = nm_setting_ip_tunnel_get_tos(s_ip_tunnel);
+        lnk_gre.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel);
+
+        val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                           10,
+                                           0,
+                                           G_MAXUINT32,
+                                           -1);
+        if (val != -1) {
+            lnk_gre.input_key   = val;
+            lnk_gre.input_flags = NM_GRE_KEY;
+        }
+
+        val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                           10,
+                                           0,
+                                           G_MAXUINT32,
+                                           -1);
+        if (val != -1) {
+            lnk_gre.output_key   = val;
+            lnk_gre.output_flags = NM_GRE_KEY;
+        }
+
+        r = nm_platform_link_gre_add(nm_device_get_platform(device),
+                                     iface,
+                                     mac_address_valid ? mac_address : NULL,
+                                     mac_address_valid ? ETH_ALEN : 0,
+                                     &lnk_gre,
+                                     out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create GRE interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    case NM_IP_TUNNEL_MODE_SIT:
+        if (parent)
+            lnk_sit.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET, str, &lnk_sit.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        g_assert(str);
+        inet_pton(AF_INET, str, &lnk_sit.remote);
+
+        lnk_sit.ttl                = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel);
+        lnk_sit.tos                = nm_setting_ip_tunnel_get_tos(s_ip_tunnel);
+        lnk_sit.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel);
+
+        r = nm_platform_link_sit_add(nm_device_get_platform(device), iface, &lnk_sit, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create SIT interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    case NM_IP_TUNNEL_MODE_IPIP:
+        if (parent)
+            lnk_ipip.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET, str, &lnk_ipip.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        g_assert(str);
+        inet_pton(AF_INET, str, &lnk_ipip.remote);
+
+        lnk_ipip.ttl                = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel);
+        lnk_ipip.tos                = nm_setting_ip_tunnel_get_tos(s_ip_tunnel);
+        lnk_ipip.path_mtu_discovery = nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel);
+
+        r = nm_platform_link_ipip_add(nm_device_get_platform(device), iface, &lnk_ipip, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create IPIP interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    case NM_IP_TUNNEL_MODE_IPIP6:
+    case NM_IP_TUNNEL_MODE_IP6IP6:
+    case NM_IP_TUNNEL_MODE_IP6GRE:
+    case NM_IP_TUNNEL_MODE_IP6GRETAP:
+        if (parent)
+            lnk_ip6tnl.parent_ifindex = nm_device_get_ifindex(parent);
+
+        str = nm_setting_ip_tunnel_get_local(s_ip_tunnel);
+        if (str)
+            inet_pton(AF_INET6, str, &lnk_ip6tnl.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        g_assert(str);
+        inet_pton(AF_INET6, str, &lnk_ip6tnl.remote);
+
+        lnk_ip6tnl.ttl         = nm_setting_ip_tunnel_get_ttl(s_ip_tunnel);
+        lnk_ip6tnl.tclass      = nm_setting_ip_tunnel_get_tos(s_ip_tunnel);
+        lnk_ip6tnl.encap_limit = nm_setting_ip_tunnel_get_encapsulation_limit(s_ip_tunnel);
+        lnk_ip6tnl.flow_label  = nm_setting_ip_tunnel_get_flow_label(s_ip_tunnel);
+        lnk_ip6tnl.flags =
+            ip6tnl_flags_setting_to_plat(nm_setting_ip_tunnel_get_flags(s_ip_tunnel));
+
+        if (NM_IN_SET(mode, NM_IP_TUNNEL_MODE_IP6GRE, NM_IP_TUNNEL_MODE_IP6GRETAP)) {
+            val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                               10,
+                                               0,
+                                               G_MAXUINT32,
+                                               -1);
+            if (val != -1) {
+                lnk_ip6tnl.input_key   = val;
+                lnk_ip6tnl.input_flags = NM_GRE_KEY;
+            }
+
+            val = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                               10,
+                                               0,
+                                               G_MAXUINT32,
+                                               -1);
+            if (val != -1) {
+                lnk_ip6tnl.output_key   = val;
+                lnk_ip6tnl.output_flags = NM_GRE_KEY;
+            }
+
+            lnk_ip6tnl.is_gre = TRUE;
+            lnk_ip6tnl.is_tap = (mode == NM_IP_TUNNEL_MODE_IP6GRETAP);
+
+            r = nm_platform_link_ip6gre_add(nm_device_get_platform(device),
+                                            iface,
+                                            mac_address_valid ? mac_address : NULL,
+                                            mac_address_valid ? ETH_ALEN : 0,
+                                            &lnk_ip6tnl,
+                                            out_plink);
+        } else {
+            lnk_ip6tnl.proto = nm_setting_ip_tunnel_get_mode(s_ip_tunnel) == NM_IP_TUNNEL_MODE_IPIP6
+                                   ? IPPROTO_IPIP
+                                   : IPPROTO_IPV6;
+            r                = nm_platform_link_ip6tnl_add(nm_device_get_platform(device),
+                                            iface,
+                                            &lnk_ip6tnl,
+                                            out_plink);
+        }
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create IPv6 tunnel interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    default:
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create IP tunnel interface '%s' for '%s': mode %d not supported",
+                    iface,
+                    nm_connection_get_id(connection),
+                    (int) nm_setting_ip_tunnel_get_mode(s_ip_tunnel));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_IP_TUNNEL,
+                                                        out_source);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)->unrealize_notify(device);
+
+    update_properties_from_ifindex(device, 0);
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    /* Only handle ip-tunnel setting here, delegate other settings to parent class */
+    if (nm_streq(setting_name, NM_SETTING_IP_TUNNEL_SETTING_NAME)) {
+        return nm_device_hash_check_invalid_keys(
+            diffs,
+            NM_SETTING_IP_TUNNEL_SETTING_NAME,
+            error,
+            NM_SETTING_IP_TUNNEL_MTU); /* reapplied with IP config */
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceIPTunnel *       self = NM_DEVICE_IP_TUNNEL(device);
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
+
+    if (_nm_ip_tunnel_mode_is_layer2(priv->mode)
+        && !nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) {
+        *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODE:
+        g_value_set_uint(value, priv->mode);
+        break;
+    case PROP_LOCAL:
+        g_value_set_string(value, priv->local);
+        break;
+    case PROP_REMOTE:
+        g_value_set_string(value, priv->remote);
+        break;
+    case PROP_TTL:
+        g_value_set_uchar(value, priv->ttl);
+        break;
+    case PROP_TOS:
+        g_value_set_uchar(value, priv->tos);
+        break;
+    case PROP_PATH_MTU_DISCOVERY:
+        g_value_set_boolean(value, priv->path_mtu_discovery);
+        break;
+    case PROP_INPUT_KEY:
+        g_value_set_string(value, priv->input_key);
+        break;
+    case PROP_OUTPUT_KEY:
+        g_value_set_string(value, priv->output_key);
+        break;
+    case PROP_ENCAPSULATION_LIMIT:
+        g_value_set_uchar(value, priv->encap_limit);
+        break;
+    case PROP_FLOW_LABEL:
+        g_value_set_uint(value, priv->flow_label);
+        break;
+    case PROP_FLAGS:
+        g_value_set_uint(value, priv->flags);
+        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)
+{
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODE:
+        priv->mode = g_value_get_uint(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_ip_tunnel_init(NMDeviceIPTunnel *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(object);
+
+    if (NM_IN_SET(priv->mode,
+                  NM_IP_TUNNEL_MODE_IPIP6,
+                  NM_IP_TUNNEL_MODE_IP6IP6,
+                  NM_IP_TUNNEL_MODE_IP6GRE,
+                  NM_IP_TUNNEL_MODE_IP6GRETAP))
+        priv->addr_family = AF_INET6;
+    else
+        priv->addr_family = AF_INET;
+
+    G_OBJECT_CLASS(nm_device_ip_tunnel_parent_class)->constructed(object);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceIPTunnel *       self = NM_DEVICE_IP_TUNNEL(object);
+    NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
+
+    nm_clear_g_free(&priv->local);
+    nm_clear_g_free(&priv->remote);
+    nm_clear_g_free(&priv->input_key);
+    nm_clear_g_free(&priv->output_key);
+
+    G_OBJECT_CLASS(nm_device_ip_tunnel_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_ip_tunnel = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_IP_TUNNEL,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_DEVICE_IP_TUNNEL_MODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Local",
+                                                             "s",
+                                                             NM_DEVICE_IP_TUNNEL_LOCAL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Remote",
+                                                             "s",
+                                                             NM_DEVICE_IP_TUNNEL_REMOTE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ttl", "y", NM_DEVICE_IP_TUNNEL_TTL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tos", "y", NM_DEVICE_IP_TUNNEL_TOS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L(
+                "PathMtuDiscovery",
+                "b",
+                NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("InputKey",
+                                                             "s",
+                                                             NM_DEVICE_IP_TUNNEL_INPUT_KEY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("OutputKey",
+                                                             "s",
+                                                             NM_DEVICE_IP_TUNNEL_OUTPUT_KEY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L(
+                "EncapsulationLimit",
+                "y",
+                NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FlowLabel",
+                                                             "u",
+                                                             NM_DEVICE_IP_TUNNEL_FLOW_LABEL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Flags",
+                                                             "u",
+                                                             NM_DEVICE_IP_TUNNEL_FLAGS), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ip_tunnel);
+
+    device_class->connection_type_supported        = NM_SETTING_IP_TUNNEL_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_IP_TUNNEL_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_GRE,
+                                                           NM_LINK_TYPE_GRETAP,
+                                                           NM_LINK_TYPE_IP6TNL,
+                                                           NM_LINK_TYPE_IP6GRE,
+                                                           NM_LINK_TYPE_IP6GRETAP,
+                                                           NM_LINK_TYPE_IPIP,
+                                                           NM_LINK_TYPE_SIT);
+
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->link_changed                = link_changed;
+    device_class->can_reapply_change          = can_reapply_change;
+    device_class->complete_connection         = complete_connection;
+    device_class->update_connection           = update_connection;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->get_configured_mtu          = get_configured_mtu;
+    device_class->unrealize_notify            = unrealize_notify;
+
+    obj_properties[PROP_MODE] =
+        g_param_spec_uint(NM_DEVICE_IP_TUNNEL_MODE,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT,
+                          0,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LOCAL] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_LOCAL,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_REMOTE] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_REMOTE,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_TTL,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_TOS,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PATH_MTU_DISCOVERY] =
+        g_param_spec_boolean(NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_INPUT_KEY] = g_param_spec_string(NM_DEVICE_IP_TUNNEL_INPUT_KEY,
+                                                         "",
+                                                         "",
+                                                         NULL,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_OUTPUT_KEY] =
+        g_param_spec_string(NM_DEVICE_IP_TUNNEL_OUTPUT_KEY,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ENCAPSULATION_LIMIT] =
+        g_param_spec_uchar(NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT,
+                           "",
+                           "",
+                           0,
+                           255,
+                           0,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_FLOW_LABEL] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FLOW_LABEL,
+                                                        "",
+                                                        "",
+                                                        0,
+                                                        (1 << 20) - 1,
+                                                        0,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FLAGS,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT32,
+                                                   0,
+                                                   G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_IP_TUNNEL_DEVICE_FACTORY (nm_ip_tunnel_device_factory_get_type())
+#define NM_IP_TUNNEL_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IP_TUNNEL_DEVICE_FACTORY, NMIPTunnelDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    NMSettingIPTunnel *s_ip_tunnel;
+    NMIPTunnelMode     mode;
+    NMLinkType         link_type;
+
+    if (connection) {
+        s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+        mode        = nm_setting_ip_tunnel_get_mode(s_ip_tunnel);
+        link_type   = tunnel_mode_to_link_type(mode);
+    } else {
+        link_type = plink->type;
+        mode      = platform_link_to_tunnel_mode(plink);
+    }
+
+    if (mode == NM_IP_TUNNEL_MODE_UNKNOWN || link_type == NM_LINK_TYPE_UNKNOWN)
+        return NULL;
+
+    return g_object_new(NM_TYPE_DEVICE_IP_TUNNEL,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "IPTunnel",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_IP_TUNNEL,
+                        NM_DEVICE_LINK_TYPE,
+                        link_type,
+                        NM_DEVICE_IP_TUNNEL_MODE,
+                        mode,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingIPTunnel *s_ip_tunnel;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_IP_TUNNEL_SETTING_NAME),
+                         NULL);
+
+    s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    g_assert(s_ip_tunnel);
+
+    return nm_setting_ip_tunnel_get_parent(s_ip_tunnel);
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    const char *       ifname;
+    NMSettingIPTunnel *s_ip_tunnel;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_IP_TUNNEL_SETTING_NAME),
+                         NULL);
+
+    s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    g_assert(s_ip_tunnel);
+
+    if (nm_setting_ip_tunnel_get_parent(s_ip_tunnel) && !parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+
+    return g_strdup(ifname);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    IP_TUNNEL,
+    IPTunnel,
+    ip_tunnel,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_GRE,
+                                         NM_LINK_TYPE_GRETAP,
+                                         NM_LINK_TYPE_SIT,
+                                         NM_LINK_TYPE_IPIP,
+                                         NM_LINK_TYPE_IP6TNL,
+                                         NM_LINK_TYPE_IP6GRE,
+                                         NM_LINK_TYPE_IP6GRETAP)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_IP_TUNNEL_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-ip-tunnel.h b/src/core/devices/nm-device-ip-tunnel.h
new file mode 100644
index 00000000..0a941cc2
--- /dev/null
+++ b/src/core/devices/nm-device-ip-tunnel.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__
+#define __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__
+
+#include "nm-core-types.h"
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_IP_TUNNEL (nm_device_ip_tunnel_get_type())
+#define NM_DEVICE_IP_TUNNEL(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnel))
+#define NM_DEVICE_IP_TUNNEL_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnelClass))
+#define NM_IS_DEVICE_IP_TUNNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IP_TUNNEL))
+#define NM_IS_DEVICE_IP_TUNNEL_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_IP_TUNNEL))
+#define NM_DEVICE_IP_TUNNEL_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_IP_TUNNEL, NMDeviceIPTunnelClass))
+
+#define NM_DEVICE_IP_TUNNEL_MODE                "mode"
+#define NM_DEVICE_IP_TUNNEL_LOCAL               "local"
+#define NM_DEVICE_IP_TUNNEL_REMOTE              "remote"
+#define NM_DEVICE_IP_TUNNEL_TTL                 "ttl"
+#define NM_DEVICE_IP_TUNNEL_TOS                 "tos"
+#define NM_DEVICE_IP_TUNNEL_PATH_MTU_DISCOVERY  "path-mtu-discovery"
+#define NM_DEVICE_IP_TUNNEL_INPUT_KEY           "input-key"
+#define NM_DEVICE_IP_TUNNEL_OUTPUT_KEY          "output-key"
+#define NM_DEVICE_IP_TUNNEL_ENCAPSULATION_LIMIT "encapsulation-limit"
+#define NM_DEVICE_IP_TUNNEL_FLOW_LABEL          "flow-label"
+#define NM_DEVICE_IP_TUNNEL_FLAGS               "flags"
+
+typedef struct _NMDeviceIPTunnel      NMDeviceIPTunnel;
+typedef struct _NMDeviceIPTunnelClass NMDeviceIPTunnelClass;
+
+GType nm_device_ip_tunnel_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_IP_TUNNEL_H__ */
diff --git a/src/core/devices/nm-device-logging.h b/src/core/devices/nm-device-logging.h
new file mode 100644
index 00000000..844e9949
--- /dev/null
+++ b/src/core/devices/nm-device-logging.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_LOGGING_H__
+#define __NETWORKMANAGER_DEVICE_LOGGING_H__
+
+#include "nm-device.h"
+
+#if !_NM_CC_SUPPORT_GENERIC
+    #define _NM_DEVICE_CAST(self) ((NMDevice *) (self))
+#elif !defined(_NMLOG_DEVICE_TYPE)
+    #define _NM_DEVICE_CAST(self)                         \
+        _Generic((self), NMDevice *                       \
+                 : ((NMDevice *) (self)), NMDevice *const \
+                 : ((NMDevice *) (self)))
+#else
+    #define _NM_DEVICE_CAST(self) \
+        _Generic((self), \
+                 _NMLOG_DEVICE_TYPE *      : ((NMDevice *) (self)), \
+                 _NMLOG_DEVICE_TYPE * const: ((NMDevice *) (self)), \
+                 NMDevice *                : ((NMDevice *) (self)), \
+                 NMDevice *           const: ((NMDevice *) (self)))
+#endif
+
+#undef _NMLOG_ENABLED
+#define _NMLOG_ENABLED(level, domain) (nm_logging_enabled((level), (domain)))
+#define _NMLOG(level, domain, ...)                                                       \
+    G_STMT_START                                                                         \
+    {                                                                                    \
+        const NMLogLevel  _level  = (level);                                             \
+        const NMLogDomain _domain = (domain);                                            \
+                                                                                         \
+        if (nm_logging_enabled(_level, _domain)) {                                       \
+            typeof(*self) *const _self   = (self);                                       \
+            const char *const    _ifname = _nm_device_get_iface(_NM_DEVICE_CAST(_self)); \
+                                                                                         \
+            nm_log_obj(_level,                                                           \
+                       _domain,                                                          \
+                       _ifname,                                                          \
+                       NULL,                                                             \
+                       _self,                                                            \
+                       "device",                                                         \
+                       "%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                    \
+                       NM_PRINT_FMT_QUOTED(_ifname, "(", _ifname, ")", "[null]")         \
+                           _NM_UTILS_MACRO_REST(__VA_ARGS__));                           \
+        }                                                                                \
+    }                                                                                    \
+    G_STMT_END
+
+#endif /* __NETWORKMANAGER_DEVICE_LOGGING_H__ */
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
new file mode 100644
index 00000000..51b820a1
--- /dev/null
+++ b/src/core/devices/nm-device-macsec.c
@@ -0,0 +1,1090 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-macsec.h"
+
+#include <linux/if_ether.h>
+
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-manager.h"
+#include "nm-setting-macsec.h"
+#include "nm-core-internal.h"
+#include "supplicant/nm-supplicant-manager.h"
+#include "supplicant/nm-supplicant-interface.h"
+#include "supplicant/nm-supplicant-config.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceMacsec
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+#define SUPPLICANT_LNK_TIMEOUT_SEC 15
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacsec,
+                             PROP_SCI,
+                             PROP_CIPHER_SUITE,
+                             PROP_ICV_LENGTH,
+                             PROP_WINDOW,
+                             PROP_ENCODING_SA,
+                             PROP_ENCRYPT,
+                             PROP_PROTECT,
+                             PROP_INCLUDE_SCI,
+                             PROP_ES,
+                             PROP_SCB,
+                             PROP_REPLAY_PROTECT,
+                             PROP_VALIDATION, );
+
+typedef struct {
+    NMPlatformLnkMacsec props;
+    gulong              parent_state_id;
+    gulong              parent_mtu_id;
+
+    struct {
+        NMSupplicantManager *        mgr;
+        NMSupplMgrCreateIfaceHandle *create_handle;
+        NMSupplicantInterface *      iface;
+
+        gulong iface_state_id;
+
+        guint con_timeout_id;
+        guint lnk_timeout_id;
+
+        bool is_associated : 1;
+    } supplicant;
+
+    NMActRequestGetSecretsCallId *macsec_secrets_id;
+} NMDeviceMacsecPrivate;
+
+struct _NMDeviceMacsec {
+    NMDevice              parent;
+    NMDeviceMacsecPrivate _priv;
+};
+
+struct _NMDeviceMacsecClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceMacsec, nm_device_macsec, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_MACSEC_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceMacsec, NM_IS_DEVICE_MACSEC, NMDevice)
+
+/******************************************************************/
+
+static void macsec_secrets_cancel(NMDeviceMacsec *self);
+
+/******************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(validation_mode_to_string,
+                                  guint8,
+                                  NM_UTILS_LOOKUP_DEFAULT_WARN("<unknown>"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(0, "disable"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(1, "check"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(2, "strict"), );
+
+static void
+parent_state_changed(NMDevice *          parent,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason,
+                     gpointer            user_data)
+{
+    NMDeviceMacsec *self = NM_DEVICE_MACSEC(user_data);
+
+    /* We'll react to our own carrier state notifications. Ignore the parent's. */
+    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
+        return;
+
+    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
+                                     NM_UNMANAGED_PARENT,
+                                     !nm_device_get_managed(parent, FALSE),
+                                     reason);
+}
+
+static void
+parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
+{
+    /* the MTU of a MACsec device is limited by the parent's MTU.
+     *
+     * When the parent's MTU changes, try to re-set the MTU. */
+    nm_device_commit_mtu(user_data);
+}
+
+static void
+parent_changed_notify(NMDevice *device,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    NMDeviceMacsec *       self = NM_DEVICE_MACSEC(device);
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    NM_DEVICE_CLASS(nm_device_macsec_parent_class)
+        ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
+
+    /*  note that @self doesn't have to clear @parent_state_id on dispose,
+     *  because NMDevice's dispose() will unset the parent, which in turn calls
+     *  parent_changed_notify(). */
+    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
+    nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
+
+    if (new_parent) {
+        priv->parent_state_id = g_signal_connect(new_parent,
+                                                 NM_DEVICE_STATE_CHANGED,
+                                                 G_CALLBACK(parent_state_changed),
+                                                 device);
+        priv->parent_mtu_id   = g_signal_connect(new_parent,
+                                               "notify::" NM_DEVICE_MTU,
+                                               G_CALLBACK(parent_mtu_maybe_changed),
+                                               device);
+
+        /* Set parent-dependent unmanaged flag */
+        nm_device_set_unmanaged_by_flags(device,
+                                         NM_UNMANAGED_PARENT,
+                                         !nm_device_get_managed(new_parent, FALSE),
+                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
+    }
+
+    /* Recheck availability now that the parent has changed */
+    if (new_ifindex > 0) {
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED);
+    }
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceMacsec *           self;
+    NMDeviceMacsecPrivate *    priv;
+    const NMPlatformLink *     plink = NULL;
+    const NMPlatformLnkMacsec *props = NULL;
+    int                        ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE_MACSEC(device));
+    self = NM_DEVICE_MACSEC(device);
+    priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    ifindex = nm_device_get_ifindex(device);
+    g_return_if_fail(ifindex > 0);
+    props = nm_platform_link_get_lnk_macsec(nm_device_get_platform(device), ifindex, &plink);
+
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get macsec properties");
+        return;
+    }
+
+    g_object_freeze_notify((GObject *) device);
+
+    if (priv->props.parent_ifindex != props->parent_ifindex)
+        nm_device_parent_set_ifindex(device, props->parent_ifindex);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(sci, PROP_SCI);
+    CHECK_PROPERTY_CHANGED(cipher_suite, PROP_CIPHER_SUITE);
+    CHECK_PROPERTY_CHANGED(window, PROP_WINDOW);
+    CHECK_PROPERTY_CHANGED(icv_length, PROP_ICV_LENGTH);
+    CHECK_PROPERTY_CHANGED(encoding_sa, PROP_ENCODING_SA);
+    CHECK_PROPERTY_CHANGED(validation, PROP_VALIDATION);
+    CHECK_PROPERTY_CHANGED(encrypt, PROP_ENCRYPT);
+    CHECK_PROPERTY_CHANGED(protect, PROP_PROTECT);
+    CHECK_PROPERTY_CHANGED(include_sci, PROP_INCLUDE_SCI);
+    CHECK_PROPERTY_CHANGED(es, PROP_ES);
+    CHECK_PROPERTY_CHANGED(scb, PROP_SCB);
+    CHECK_PROPERTY_CHANGED(replay_protect, PROP_REPLAY_PROTECT);
+
+    g_object_thaw_notify((GObject *) device);
+}
+
+static NMSupplicantConfig *
+build_supplicant_config(NMDeviceMacsec *self, GError **error)
+{
+    gs_unref_object NMSupplicantConfig *config = NULL;
+    NMSettingMacsec *                   s_macsec;
+    NMSetting8021x *                    s_8021x;
+    NMConnection *                      connection;
+    const char *                        con_uuid;
+    guint32                             mtu;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    g_return_val_if_fail(connection, NULL);
+
+    con_uuid = nm_connection_get_uuid(connection);
+    mtu      = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
+                                   nm_device_get_ifindex(NM_DEVICE(self)));
+
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+
+    s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC);
+
+    g_return_val_if_fail(s_macsec, NULL);
+
+    if (!nm_supplicant_config_add_setting_macsec(config, s_macsec, error)) {
+        g_prefix_error(error, "macsec-setting: ");
+        return NULL;
+    }
+
+    if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) {
+        s_8021x = nm_connection_get_setting_802_1x(connection);
+        if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) {
+            g_prefix_error(error, "802-1x-setting: ");
+            return NULL;
+        }
+    }
+
+    return g_steal_pointer(&config);
+}
+
+static void
+supplicant_interface_release(NMDeviceMacsec *self)
+{
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->supplicant.create_handle,
+                     nm_supplicant_manager_create_interface_cancel);
+
+    nm_clear_g_source(&priv->supplicant.lnk_timeout_id);
+    nm_clear_g_source(&priv->supplicant.con_timeout_id);
+    nm_clear_g_signal_handler(priv->supplicant.iface, &priv->supplicant.iface_state_id);
+
+    if (priv->supplicant.iface) {
+        nm_supplicant_interface_disconnect(priv->supplicant.iface);
+        g_clear_object(&priv->supplicant.iface);
+    }
+}
+
+static void
+macsec_secrets_cb(NMActRequest *                req,
+                  NMActRequestGetSecretsCallId *call_id,
+                  NMSettingsConnection *        connection,
+                  GError *                      error,
+                  gpointer                      user_data)
+{
+    NMDeviceMacsec *       self   = NM_DEVICE_MACSEC(user_data);
+    NMDevice *             device = NM_DEVICE(self);
+    NMDeviceMacsecPrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE_MACSEC(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->macsec_secrets_id == call_id);
+
+    priv->macsec_secrets_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == connection);
+
+    if (error) {
+        _LOGW(LOGD_ETHER, "%s", error->message);
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+macsec_secrets_cancel(NMDeviceMacsec *self)
+{
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    if (priv->macsec_secrets_id)
+        nm_act_request_cancel_secrets(NULL, priv->macsec_secrets_id);
+    nm_assert(!priv->macsec_secrets_id);
+}
+
+static void
+macsec_secrets_get_secrets(NMDeviceMacsec *             self,
+                           const char *                 setting_name,
+                           NMSecretAgentGetSecretsFlags flags)
+{
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    NMActRequest *         req;
+
+    macsec_secrets_cancel(self);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv->macsec_secrets_id =
+        nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, macsec_secrets_cb, self);
+    g_return_if_fail(priv->macsec_secrets_id);
+}
+
+static gboolean
+supplicant_lnk_timeout_cb(gpointer user_data)
+{
+    NMDeviceMacsec *       self = NM_DEVICE_MACSEC(user_data);
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    NMDevice *             dev  = NM_DEVICE(self);
+    NMActRequest *         req;
+    NMConnection *         applied_connection;
+    const char *           setting_name;
+
+    priv->supplicant.lnk_timeout_id = 0;
+
+    req = nm_device_get_act_request(dev);
+
+    if (nm_device_get_state(dev) == NM_DEVICE_STATE_ACTIVATED) {
+        nm_device_state_changed(dev,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
+        return G_SOURCE_REMOVE;
+    }
+
+    /* Disconnect event during initial authentication and credentials
+     * ARE checked - we are likely to have wrong key.  Ask the user for
+     * another one.
+     */
+    if (nm_device_get_state(dev) != NM_DEVICE_STATE_CONFIG)
+        goto time_out;
+
+    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    setting_name       = nm_connection_need_secrets(applied_connection, NULL);
+    if (!setting_name)
+        goto time_out;
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER,
+          "Activation: disconnected during authentication, asking for new key.");
+    supplicant_interface_release(self);
+
+    nm_device_state_changed(dev,
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+    macsec_secrets_get_secrets(self, setting_name, NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
+
+    return G_SOURCE_REMOVE;
+
+time_out:
+    _LOGW(LOGD_DEVICE | LOGD_ETHER, "link timed out.");
+    nm_device_state_changed(dev,
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+supplicant_iface_state_is_completed(NMDeviceMacsec *self, NMSupplicantInterfaceState state)
+{
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    if (state == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) {
+        nm_clear_g_source(&priv->supplicant.lnk_timeout_id);
+        nm_clear_g_source(&priv->supplicant.con_timeout_id);
+
+        nm_device_bring_up(NM_DEVICE(self), TRUE, NULL);
+
+        /* If this is the initial association during device activation,
+         * schedule the next activation stage.
+         */
+        if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) {
+            _LOGI(LOGD_DEVICE, "Activation: Stage 2 of 5 (Device Configure) successful.");
+            nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self));
+        }
+        return;
+    }
+
+    if (!priv->supplicant.lnk_timeout_id && !priv->supplicant.con_timeout_id)
+        priv->supplicant.lnk_timeout_id =
+            g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self);
+}
+
+static void
+supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data)
+{
+    NMDeviceMacsec *       self;
+    NMDeviceMacsecPrivate *priv;
+
+    if (nm_utils_error_is_cancelled_or_disposing(error))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    if (error) {
+        supplicant_interface_release(self);
+        nm_device_queue_state(NM_DEVICE(self),
+                              NM_DEVICE_STATE_FAILED,
+                              NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED);
+        return;
+    }
+
+    nm_assert(!priv->supplicant.lnk_timeout_id);
+    nm_assert(!priv->supplicant.is_associated);
+
+    priv->supplicant.is_associated = TRUE;
+    supplicant_iface_state_is_completed(self,
+                                        nm_supplicant_interface_get_state(priv->supplicant.iface));
+}
+
+static gboolean
+supplicant_iface_start(NMDeviceMacsec *self)
+{
+    NMDeviceMacsecPrivate *priv                = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    gs_unref_object NMSupplicantConfig *config = NULL;
+    gs_free_error GError *error                = NULL;
+
+    config = build_supplicant_config(self, &error);
+    if (!config) {
+        _LOGE(LOGD_DEVICE, "Activation: couldn't build security configuration: %s", error->message);
+        supplicant_interface_release(self);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED);
+        return FALSE;
+    }
+
+    nm_supplicant_interface_disconnect(priv->supplicant.iface);
+    nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self);
+    return TRUE;
+}
+
+static void
+supplicant_iface_state_cb(NMSupplicantInterface *iface,
+                          int                    new_state_i,
+                          int                    old_state_i,
+                          int                    disconnect_reason,
+                          gpointer               user_data)
+{
+    NMDeviceMacsec *           self      = NM_DEVICE_MACSEC(user_data);
+    NMDeviceMacsecPrivate *    priv      = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    NMSupplicantInterfaceState new_state = new_state_i;
+    NMSupplicantInterfaceState old_state = old_state_i;
+
+    _LOGI(LOGD_DEVICE,
+          "supplicant interface state: %s -> %s",
+          nm_supplicant_interface_state_to_string(old_state),
+          nm_supplicant_interface_state_to_string(new_state));
+
+    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
+        supplicant_interface_release(self);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        if (!supplicant_iface_start(self))
+            return;
+    }
+
+    if (priv->supplicant.is_associated)
+        supplicant_iface_state_is_completed(self, new_state);
+}
+
+static gboolean
+handle_auth_or_fail(NMDeviceMacsec *self, NMActRequest *req, gboolean new_secrets)
+{
+    const char *  setting_name;
+    NMConnection *applied_connection;
+
+    if (!nm_device_auth_retries_try_next(NM_DEVICE(self)))
+        return FALSE;
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_NONE);
+
+    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    setting_name       = nm_connection_need_secrets(applied_connection, NULL);
+    if (!setting_name) {
+        _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets.");
+        return FALSE;
+    }
+
+    macsec_secrets_get_secrets(
+        self,
+        setting_name,
+        NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+            | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0));
+    return TRUE;
+}
+
+static gboolean
+supplicant_connection_timeout_cb(gpointer user_data)
+{
+    NMDeviceMacsec *       self   = NM_DEVICE_MACSEC(user_data);
+    NMDeviceMacsecPrivate *priv   = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    NMDevice *             device = NM_DEVICE(self);
+    NMActRequest *         req;
+    NMSettingsConnection * connection;
+    guint64                timestamp   = 0;
+    gboolean               new_secrets = TRUE;
+
+    priv->supplicant.con_timeout_id = 0;
+
+    /* Authentication failed; either driver problems, the encryption key is
+     * wrong, the passwords or certificates were wrong or the Ethernet switch's
+     * port is not configured for 802.1x. */
+    _LOGW(LOGD_DEVICE, "Activation: (macsec) association took too long.");
+
+    supplicant_interface_release(self);
+
+    req        = nm_device_get_act_request(device);
+    connection = nm_act_request_get_settings_connection(req);
+    g_return_val_if_fail(connection, G_SOURCE_REMOVE);
+
+    /* Ask for new secrets only if we've never activated this connection
+     * before.  If we've connected before, don't bother the user with dialogs,
+     * just retry or fail, and if we never connect the user can fix the
+     * password somewhere else. */
+    if (nm_settings_connection_get_timestamp(connection, &timestamp))
+        new_secrets = !timestamp;
+
+    if (!handle_auth_or_fail(self, req, new_secrets)) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return G_SOURCE_REMOVE;
+    }
+
+    _LOGW(LOGD_DEVICE, "Activation: (macsec) asking for new secrets");
+
+    if (!priv->supplicant.lnk_timeout_id && priv->supplicant.iface) {
+        NMSupplicantInterfaceState state;
+
+        state = nm_supplicant_interface_get_state(priv->supplicant.iface);
+        if (state != NM_SUPPLICANT_INTERFACE_STATE_COMPLETED
+            && nm_supplicant_interface_state_is_operational(state))
+            priv->supplicant.lnk_timeout_id =
+                g_timeout_add_seconds(SUPPLICANT_LNK_TIMEOUT_SEC, supplicant_lnk_timeout_cb, self);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+supplicant_interface_create_cb(NMSupplicantManager *        supplicant_manager,
+                               NMSupplMgrCreateIfaceHandle *handle,
+                               NMSupplicantInterface *      iface,
+                               GError *                     error,
+                               gpointer                     user_data)
+{
+    NMDeviceMacsec *       self;
+    NMDeviceMacsecPrivate *priv;
+    guint                  timeout;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    nm_assert(priv->supplicant.create_handle == handle);
+
+    priv->supplicant.create_handle = NULL;
+
+    if (error) {
+        _LOGE(LOGD_DEVICE, "Couldn't initialize supplicant interface: %s", error->message);
+        supplicant_interface_release(self);
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    priv->supplicant.iface         = g_object_ref(iface);
+    priv->supplicant.is_associated = FALSE;
+
+    priv->supplicant.iface_state_id = g_signal_connect(priv->supplicant.iface,
+                                                       NM_SUPPLICANT_INTERFACE_STATE,
+                                                       G_CALLBACK(supplicant_iface_state_cb),
+                                                       self);
+
+    timeout = nm_device_get_supplicant_timeout(NM_DEVICE(self));
+    priv->supplicant.con_timeout_id =
+        g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self);
+
+    if (nm_supplicant_interface_state_is_operational(nm_supplicant_interface_get_state(iface)))
+        supplicant_iface_start(self);
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceMacsec *       self = NM_DEVICE_MACSEC(device);
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+    NMConnection *         connection;
+    NMDevice *             parent;
+    const char *           setting_name;
+    int                    ifindex;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (!priv->supplicant.mgr)
+        priv->supplicant.mgr = g_object_ref(nm_supplicant_manager_get());
+
+    /* If we need secrets, get them */
+    setting_name = nm_connection_need_secrets(connection, NULL);
+    if (setting_name) {
+        NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self));
+
+        _LOGI(LOGD_DEVICE,
+              "Activation: connection '%s' has security, but secrets are required.",
+              nm_connection_get_id(connection));
+
+        if (!handle_auth_or_fail(self, req, FALSE)) {
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_ETHER,
+          "Activation: connection '%s' requires no security. No secrets needed.",
+          nm_connection_get_id(connection));
+
+    supplicant_interface_release(self);
+
+    parent = nm_device_parent_get_device(NM_DEVICE(self));
+    g_return_val_if_fail(parent, NM_ACT_STAGE_RETURN_FAILURE);
+    ifindex = nm_device_get_ifindex(parent);
+    g_return_val_if_fail(ifindex > 0, NM_ACT_STAGE_RETURN_FAILURE);
+
+    priv->supplicant.create_handle =
+        nm_supplicant_manager_create_interface(priv->supplicant.mgr,
+                                               ifindex,
+                                               NM_SUPPLICANT_DRIVER_MACSEC,
+                                               supplicant_interface_create_cb,
+                                               self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceMacsec *self = NM_DEVICE_MACSEC(device);
+
+    supplicant_interface_release(self);
+}
+
+/******************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    /* We assume MACsec interfaces always support carrier detect */
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+/******************************************************************/
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    if (!nm_device_parent_get_device(device))
+        return FALSE;
+    return NM_DEVICE_CLASS(nm_device_macsec_parent_class)->is_available(device, flags);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *        iface = nm_device_get_iface(device);
+    NMSettingMacsec *   s_macsec;
+    NMPlatformLnkMacsec lnk = {};
+    int                 parent_ifindex;
+    const char *        hw_addr;
+    union {
+        struct {
+            guint8  mac[6];
+            guint16 port;
+        } s;
+        guint64 u;
+    } sci;
+    int r;
+
+    s_macsec = nm_connection_get_setting_macsec(connection);
+    g_assert(s_macsec);
+
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "MACsec devices can not be created without a parent interface");
+        return FALSE;
+    }
+
+    lnk.encrypt = nm_setting_macsec_get_encrypt(s_macsec);
+
+    hw_addr = nm_device_get_hw_address(parent);
+    if (!hw_addr) {
+        g_set_error(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "can't read parent MAC");
+        return FALSE;
+    }
+
+    nm_utils_hwaddr_aton(hw_addr, sci.s.mac, ETH_ALEN);
+    sci.s.port      = htons(nm_setting_macsec_get_port(s_macsec));
+    lnk.sci         = be64toh(sci.u);
+    lnk.validation  = nm_setting_macsec_get_validation(s_macsec);
+    lnk.include_sci = nm_setting_macsec_get_send_sci(s_macsec);
+
+    parent_ifindex = nm_device_get_ifindex(parent);
+    g_warn_if_fail(parent_ifindex > 0);
+
+    r = nm_platform_link_macsec_add(nm_device_get_platform(device),
+                                    iface,
+                                    parent_ifindex,
+                                    &lnk,
+                                    out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create macsec interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+
+    return TRUE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_macsec_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    if (new_state > NM_DEVICE_STATE_ACTIVATED)
+        macsec_secrets_cancel(NM_DEVICE_MACSEC(device));
+}
+
+/******************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceMacsec *       self = NM_DEVICE_MACSEC(object);
+    NMDeviceMacsecPrivate *priv = NM_DEVICE_MACSEC_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_SCI:
+        g_value_set_uint64(value, priv->props.sci);
+        break;
+    case PROP_CIPHER_SUITE:
+        g_value_set_uint64(value, priv->props.cipher_suite);
+        break;
+    case PROP_ICV_LENGTH:
+        g_value_set_uchar(value, priv->props.icv_length);
+        break;
+    case PROP_WINDOW:
+        g_value_set_uint(value, priv->props.window);
+        break;
+    case PROP_ENCODING_SA:
+        g_value_set_uchar(value, priv->props.encoding_sa);
+        break;
+    case PROP_ENCRYPT:
+        g_value_set_boolean(value, priv->props.encrypt);
+        break;
+    case PROP_PROTECT:
+        g_value_set_boolean(value, priv->props.protect);
+        break;
+    case PROP_INCLUDE_SCI:
+        g_value_set_boolean(value, priv->props.include_sci);
+        break;
+    case PROP_ES:
+        g_value_set_boolean(value, priv->props.es);
+        break;
+    case PROP_SCB:
+        g_value_set_boolean(value, priv->props.scb);
+        break;
+    case PROP_REPLAY_PROTECT:
+        g_value_set_boolean(value, priv->props.replay_protect);
+        break;
+    case PROP_VALIDATION:
+        g_value_set_string(value, validation_mode_to_string(priv->props.validation));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+nm_device_macsec_init(NMDeviceMacsec *self)
+{}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceMacsec *self = NM_DEVICE_MACSEC(object);
+
+    macsec_secrets_cancel(self);
+    supplicant_interface_release(self);
+
+    G_OBJECT_CLASS(nm_device_macsec_parent_class)->dispose(object);
+
+    nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_state_id == 0);
+    nm_assert(NM_DEVICE_MACSEC_GET_PRIVATE(self)->parent_mtu_id == 0);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_macsec = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_MACSEC,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Sci", "t", NM_DEVICE_MACSEC_SCI),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IcvLength",
+                                                             "y",
+                                                             NM_DEVICE_MACSEC_ICV_LENGTH),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("CipherSuite",
+                                                             "t",
+                                                             NM_DEVICE_MACSEC_CIPHER_SUITE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Window",
+                                                             "u",
+                                                             NM_DEVICE_MACSEC_WINDOW),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("EncodingSa",
+                                                             "y",
+                                                             NM_DEVICE_MACSEC_ENCODING_SA),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Validation",
+                                                             "s",
+                                                             NM_DEVICE_MACSEC_VALIDATION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Encrypt",
+                                                             "b",
+                                                             NM_DEVICE_MACSEC_ENCRYPT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Protect",
+                                                             "b",
+                                                             NM_DEVICE_MACSEC_PROTECT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IncludeSci",
+                                                             "b",
+                                                             NM_DEVICE_MACSEC_INCLUDE_SCI),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Es", "b", NM_DEVICE_MACSEC_ES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Scb", "b", NM_DEVICE_MACSEC_SCB),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ReplayProtect",
+                                                             "b",
+                                                             NM_DEVICE_MACSEC_REPLAY_PROTECT), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_macsec_class_init(NMDeviceMacsecClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_macsec);
+
+    device_class->connection_type_supported        = NM_SETTING_MACSEC_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_MACSEC_SETTING_NAME;
+    device_class->link_types       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_MACSEC);
+    device_class->mtu_parent_delta = 32;
+
+    device_class->act_stage2_config        = act_stage2_config;
+    device_class->create_and_realize       = create_and_realize;
+    device_class->deactivate               = deactivate;
+    device_class->get_generic_capabilities = get_generic_capabilities;
+    device_class->link_changed             = link_changed;
+    device_class->is_available             = is_available;
+    device_class->parent_changed_notify    = parent_changed_notify;
+    device_class->state_changed            = device_state_changed;
+    device_class->get_configured_mtu       = nm_device_get_configured_mtu_wired_parent;
+
+    obj_properties[PROP_SCI] = g_param_spec_uint64(NM_DEVICE_MACSEC_SCI,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT64,
+                                                   0,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_CIPHER_SUITE] =
+        g_param_spec_uint64(NM_DEVICE_MACSEC_CIPHER_SUITE,
+                            "",
+                            "",
+                            0,
+                            G_MAXUINT64,
+                            0,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ICV_LENGTH] = g_param_spec_uchar(NM_DEVICE_MACSEC_ICV_LENGTH,
+                                                         "",
+                                                         "",
+                                                         0,
+                                                         G_MAXUINT8,
+                                                         0,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_WINDOW]     = g_param_spec_uint(NM_DEVICE_MACSEC_WINDOW,
+                                                    "",
+                                                    "",
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ENCODING_SA] =
+        g_param_spec_uchar(NM_DEVICE_MACSEC_ENCODING_SA,
+                           "",
+                           "",
+                           0,
+                           3,
+                           0,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_VALIDATION] =
+        g_param_spec_string(NM_DEVICE_MACSEC_VALIDATION,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ENCRYPT] = g_param_spec_boolean(NM_DEVICE_MACSEC_ENCRYPT,
+                                                        "",
+                                                        "",
+                                                        FALSE,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_PROTECT] = g_param_spec_boolean(NM_DEVICE_MACSEC_PROTECT,
+                                                        "",
+                                                        "",
+                                                        FALSE,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_INCLUDE_SCI] =
+        g_param_spec_boolean(NM_DEVICE_MACSEC_INCLUDE_SCI,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ES]  = g_param_spec_boolean(NM_DEVICE_MACSEC_ES,
+                                                   "",
+                                                   "",
+                                                   FALSE,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_SCB] = g_param_spec_boolean(NM_DEVICE_MACSEC_SCB,
+                                                    "",
+                                                    "",
+                                                    FALSE,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_REPLAY_PROTECT] =
+        g_param_spec_boolean(NM_DEVICE_MACSEC_REPLAY_PROTECT,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*************************************************************/
+
+#define NM_TYPE_MACSEC_DEVICE_FACTORY (nm_macsec_device_factory_get_type())
+#define NM_MACSEC_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACSEC_DEVICE_FACTORY, NMMacsecDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_MACSEC,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Macsec",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_MACSEC,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_MACSEC,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingMacsec *s_macsec;
+    NMSettingWired * s_wired;
+    const char *     parent = NULL;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACSEC_SETTING_NAME), NULL);
+
+    s_macsec = nm_connection_get_setting_macsec(connection);
+    g_assert(s_macsec);
+
+    parent = nm_setting_macsec_get_parent(s_macsec);
+    if (parent)
+        return parent;
+
+    /* Try the hardware address from the MACsec connection's hardware setting */
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (s_wired)
+        return nm_setting_wired_get_mac_address(s_wired);
+
+    return NULL;
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    NMSettingMacsec *s_macsec;
+    const char *     ifname;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACSEC_SETTING_NAME), NULL);
+
+    s_macsec = nm_connection_get_setting_macsec(connection);
+    g_assert(s_macsec);
+
+    if (!parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+    return g_strdup(ifname);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    MACSEC,
+    Macsec,
+    macsec,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_MACSEC)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_MACSEC_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;)
diff --git a/src/core/devices/nm-device-macsec.h b/src/core/devices/nm-device-macsec.h
new file mode 100644
index 00000000..e91fe51c
--- /dev/null
+++ b/src/core/devices/nm-device-macsec.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEVICE_MACSEC_H__
+#define __NM_DEVICE_MACSEC_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_MACSEC (nm_device_macsec_get_type())
+#define NM_DEVICE_MACSEC(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsec))
+#define NM_DEVICE_MACSEC_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsecClass))
+#define NM_IS_DEVICE_MACSEC(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACSEC))
+#define NM_IS_DEVICE_MACSEC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MACSEC))
+#define NM_DEVICE_MACSEC_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MACSEC, NMDeviceMacsecClass))
+
+#define NM_DEVICE_MACSEC_SCI            "sci"
+#define NM_DEVICE_MACSEC_CIPHER_SUITE   "cipher-suite"
+#define NM_DEVICE_MACSEC_ICV_LENGTH     "icv-length"
+#define NM_DEVICE_MACSEC_WINDOW         "window"
+#define NM_DEVICE_MACSEC_ENCODING_SA    "encoding-sa"
+#define NM_DEVICE_MACSEC_VALIDATION     "validation"
+#define NM_DEVICE_MACSEC_ENCRYPT        "encrypt"
+#define NM_DEVICE_MACSEC_PROTECT        "protect"
+#define NM_DEVICE_MACSEC_INCLUDE_SCI    "include-sci"
+#define NM_DEVICE_MACSEC_ES             "es"
+#define NM_DEVICE_MACSEC_SCB            "scb"
+#define NM_DEVICE_MACSEC_REPLAY_PROTECT "replay-protect"
+
+typedef struct _NMDeviceMacsec      NMDeviceMacsec;
+typedef struct _NMDeviceMacsecClass NMDeviceMacsecClass;
+
+GType nm_device_macsec_get_type(void);
+
+#endif /* __NM_DEVICE_MACSEC_H__ */
diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c
new file mode 100644
index 00000000..e8b39ed6
--- /dev/null
+++ b/src/core/devices/nm-device-macvlan.c
@@ -0,0 +1,665 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-macvlan.h"
+
+#include <linux/if_link.h>
+
+#include "nm-device-private.h"
+#include "settings/nm-settings.h"
+#include "nm-act-request.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-macvlan.h"
+#include "nm-setting-wired.h"
+#include "nm-active-connection.h"
+#include "nm-ip4-config.h"
+#include "nm-utils.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceMacvlan
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacvlan, PROP_MODE, PROP_NO_PROMISC, PROP_TAP, );
+
+typedef struct {
+    gulong               parent_state_id;
+    gulong               parent_mtu_id;
+    NMPlatformLnkMacvlan props;
+} NMDeviceMacvlanPrivate;
+
+struct _NMDeviceMacvlan {
+    NMDevice               parent;
+    NMDeviceMacvlanPrivate _priv;
+};
+
+struct _NMDeviceMacvlanClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceMacvlan, nm_device_macvlan, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_MACVLAN_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceMacvlan, NM_IS_DEVICE_MACVLAN, NMDevice)
+
+/*****************************************************************************/
+
+static int modes[][2] = {
+    {NM_SETTING_MACVLAN_MODE_VEPA, MACVLAN_MODE_VEPA},
+    {NM_SETTING_MACVLAN_MODE_BRIDGE, MACVLAN_MODE_BRIDGE},
+    {NM_SETTING_MACVLAN_MODE_PRIVATE, MACVLAN_MODE_PRIVATE},
+    {NM_SETTING_MACVLAN_MODE_PASSTHRU, MACVLAN_MODE_PASSTHRU},
+};
+
+static int
+setting_mode_to_platform(int mode)
+{
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS(modes); i++) {
+        if (modes[i][0] == mode)
+            return modes[i][1];
+    }
+
+    return 0;
+}
+
+static int
+platform_mode_to_setting(int mode)
+{
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS(modes); i++) {
+        if (modes[i][1] == mode)
+            return modes[i][0];
+    }
+
+    return 0;
+}
+
+static const char *
+platform_mode_to_string(guint mode)
+{
+    switch (mode) {
+    case MACVLAN_MODE_PRIVATE:
+        return "private";
+    case MACVLAN_MODE_VEPA:
+        return "vepa";
+    case MACVLAN_MODE_BRIDGE:
+        return "bridge";
+    case MACVLAN_MODE_PASSTHRU:
+        return "passthru";
+    default:
+        return "unknown";
+    }
+}
+
+/*****************************************************************************/
+
+static void
+parent_state_changed(NMDevice *          parent,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason,
+                     gpointer            user_data)
+{
+    NMDeviceMacvlan *self = NM_DEVICE_MACVLAN(user_data);
+
+    /* We'll react to our own carrier state notifications. Ignore the parent's. */
+    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
+        return;
+
+    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
+                                     NM_UNMANAGED_PARENT,
+                                     !nm_device_get_managed(parent, FALSE),
+                                     reason);
+}
+
+static void
+parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
+{
+    /* the MTU of a macvlan/macvtap device is limited by the parent's MTU.
+     *
+     * When the parent's MTU changes, try to re-set the MTU. */
+    nm_device_commit_mtu(user_data);
+}
+
+static void
+parent_changed_notify(NMDevice *device,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    NMDeviceMacvlan *       self = NM_DEVICE_MACVLAN(device);
+    NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(self);
+
+    NM_DEVICE_CLASS(nm_device_macvlan_parent_class)
+        ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
+
+    /*  note that @self doesn't have to clear @parent_state_id on dispose,
+     *  because NMDevice's dispose() will unset the parent, which in turn calls
+     *  parent_changed_notify(). */
+    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
+    nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
+
+    if (new_parent) {
+        priv->parent_state_id = g_signal_connect(new_parent,
+                                                 NM_DEVICE_STATE_CHANGED,
+                                                 G_CALLBACK(parent_state_changed),
+                                                 device);
+        priv->parent_mtu_id   = g_signal_connect(new_parent,
+                                               "notify::" NM_DEVICE_MTU,
+                                               G_CALLBACK(parent_mtu_maybe_changed),
+                                               device);
+
+        /* Set parent-dependent unmanaged flag */
+        nm_device_set_unmanaged_by_flags(device,
+                                         NM_UNMANAGED_PARENT,
+                                         !nm_device_get_managed(new_parent, FALSE),
+                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
+    }
+
+    if (new_ifindex > 0) {
+        /* Recheck availability now that the parent has changed */
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED);
+    }
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceMacvlan *           self   = NM_DEVICE_MACVLAN(device);
+    NMDeviceMacvlanPrivate *    priv   = NM_DEVICE_MACVLAN_GET_PRIVATE(self);
+    GObject *                   object = G_OBJECT(device);
+    const NMPlatformLnkMacvlan *props;
+    const NMPlatformLink *      plink;
+
+    if (priv->props.tap)
+        props = nm_platform_link_get_lnk_macvtap(nm_device_get_platform(device),
+                                                 nm_device_get_ifindex(device),
+                                                 &plink);
+    else
+        props = nm_platform_link_get_lnk_macvlan(nm_device_get_platform(device),
+                                                 nm_device_get_ifindex(device),
+                                                 &plink);
+
+    if (!props) {
+        _LOGW(LOGD_PLATFORM,
+              "could not get %s properties",
+              priv->props.tap ? "macvtap" : "macvlan");
+        return;
+    }
+
+    g_object_freeze_notify(object);
+
+    nm_device_parent_set_ifindex(device, plink->parent);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(mode, PROP_MODE);
+    CHECK_PROPERTY_CHANGED(no_promisc, PROP_NO_PROMISC);
+
+    g_object_thaw_notify(object);
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_macvlan_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *         iface = nm_device_get_iface(device);
+    NMSettingMacvlan *   s_macvlan;
+    NMPlatformLnkMacvlan lnk = {};
+    int                  parent_ifindex;
+    int                  r;
+
+    s_macvlan = nm_connection_get_setting_macvlan(connection);
+    g_return_val_if_fail(s_macvlan, FALSE);
+
+    parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0;
+
+    if (parent_ifindex <= 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "MACVLAN devices can not be created without a parent interface");
+        g_return_val_if_fail(!parent, FALSE);
+        return FALSE;
+    }
+
+    lnk.mode = setting_mode_to_platform(nm_setting_macvlan_get_mode(s_macvlan));
+    if (!lnk.mode) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "unsupported MACVLAN mode %u in connection %s",
+                    nm_setting_macvlan_get_mode(s_macvlan),
+                    nm_connection_get_uuid(connection));
+        return FALSE;
+    }
+    lnk.no_promisc = !nm_setting_macvlan_get_promiscuous(s_macvlan);
+    lnk.tap        = nm_setting_macvlan_get_tap(s_macvlan);
+
+    r = nm_platform_link_macvlan_add(nm_device_get_platform(device),
+                                     iface,
+                                     parent_ifindex,
+                                     &lnk,
+                                     out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create %s interface '%s' for '%s': %s",
+                    lnk.tap ? "macvtap" : "macvlan",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    /* We assume MACVLAN interfaces always support carrier detect */
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    if (!nm_device_parent_get_device(device))
+        return FALSE;
+    return NM_DEVICE_CLASS(nm_device_macvlan_parent_class)->is_available(device, flags);
+}
+
+/*****************************************************************************/
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(device);
+    NMSettingMacvlan *      s_macvlan;
+    const char *            parent = NULL;
+
+    if (!NM_DEVICE_CLASS(nm_device_macvlan_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_macvlan = nm_connection_get_setting_macvlan(connection);
+
+    if (nm_setting_macvlan_get_tap(s_macvlan) != priv->props.tap) {
+        if (priv->props.tap) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "macvtap device does not match macvlan profile");
+        } else {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "macvlan device does not match macvtap profile");
+        }
+        return FALSE;
+    }
+
+    /* Before the device is realized some properties will not be set */
+    if (nm_device_is_real(device)) {
+        if (setting_mode_to_platform(nm_setting_macvlan_get_mode(s_macvlan)) != priv->props.mode) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "macvlan mode setting differs");
+            return FALSE;
+        }
+
+        if (nm_setting_macvlan_get_promiscuous(s_macvlan) == priv->props.no_promisc) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "macvlan promiscuous setting differs");
+            return FALSE;
+        }
+
+        /* Check parent interface; could be an interface name or a UUID */
+        parent = nm_setting_macvlan_get_parent(s_macvlan);
+        if (parent) {
+            if (!nm_device_match_parent(device, parent)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "macvlan parent setting differs");
+                return FALSE;
+            }
+        } else {
+            /* Parent could be a MAC address in an NMSettingWired */
+            if (!nm_device_match_parent_hwaddr(device, connection, TRUE)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "macvlan parent mac setting differs");
+                return FALSE;
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingMacvlan *s_macvlan;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_MACVLAN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("MACVLAN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_macvlan = nm_connection_get_setting_macvlan(connection);
+    if (!s_macvlan) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'macvlan' setting is required.");
+        return FALSE;
+    }
+
+    /* If there's no MACVLAN interface, no parent, and no hardware address in the
+     * settings, then there's not enough information to complete the setting.
+     */
+    if (!nm_setting_macvlan_get_parent(s_macvlan)
+        && !nm_device_match_parent_hwaddr(device, connection, TRUE)) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INVALID_CONNECTION,
+            "The 'macvlan' setting had no interface name, parent, or hardware address.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceMacvlanPrivate *priv      = NM_DEVICE_MACVLAN_GET_PRIVATE(device);
+    NMSettingMacvlan *      s_macvlan = nm_connection_get_setting_macvlan(connection);
+    int                     new_mode;
+
+    if (!s_macvlan) {
+        s_macvlan = (NMSettingMacvlan *) nm_setting_macvlan_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_macvlan);
+    }
+
+    new_mode = platform_mode_to_setting(priv->props.mode);
+    if (new_mode != nm_setting_macvlan_get_mode(s_macvlan))
+        g_object_set(s_macvlan, NM_SETTING_MACVLAN_MODE, new_mode, NULL);
+
+    if (priv->props.no_promisc == nm_setting_macvlan_get_promiscuous(s_macvlan))
+        g_object_set(s_macvlan, NM_SETTING_MACVLAN_PROMISCUOUS, !priv->props.no_promisc, NULL);
+
+    if (priv->props.tap != nm_setting_macvlan_get_tap(s_macvlan))
+        g_object_set(s_macvlan, NM_SETTING_MACVLAN_TAP, !!priv->props.tap, NULL);
+
+    g_object_set(
+        s_macvlan,
+        NM_SETTING_MACVLAN_PARENT,
+        nm_device_parent_find_for_connection(device, nm_setting_macvlan_get_parent(s_macvlan)),
+        NULL);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODE:
+        g_value_set_string(value, platform_mode_to_string(priv->props.mode));
+        break;
+    case PROP_NO_PROMISC:
+        g_value_set_boolean(value, priv->props.no_promisc);
+        break;
+    case PROP_TAP:
+        g_value_set_boolean(value, priv->props.tap);
+        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)
+{
+    NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_TAP:
+        priv->props.tap = g_value_get_boolean(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_macvlan_init(NMDeviceMacvlan *self)
+{}
+
+#if NM_MORE_ASSERTS
+static void
+dispose(GObject *object)
+{
+    G_OBJECT_CLASS(nm_device_macvlan_parent_class)->dispose(object);
+
+    nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_state_id == 0);
+    nm_assert(NM_DEVICE_MACVLAN_GET_PRIVATE(object)->parent_mtu_id == 0);
+}
+#endif
+
+static const NMDBusInterfaceInfoExtended interface_info_device_macvlan = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_MACVLAN,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "s", NM_DEVICE_MACVLAN_MODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NoPromisc",
+                                                             "b",
+                                                             NM_DEVICE_MACVLAN_NO_PROMISC),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tab",
+                                                             "b",
+                                                             NM_DEVICE_MACVLAN_TAP), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_macvlan_class_init(NMDeviceMacvlanClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+#if NM_MORE_ASSERTS
+    object_class->dispose = dispose;
+#endif
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_macvlan);
+
+    device_class->connection_type_supported        = NM_SETTING_MACVLAN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_MACVLAN_SETTING_NAME;
+    device_class->link_types =
+        NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_MACVLAN, NM_LINK_TYPE_MACVTAP);
+    device_class->mtu_parent_delta = 0;
+
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->check_connection_compatible            = check_connection_compatible;
+    device_class->complete_connection                    = complete_connection;
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->get_configured_mtu    = nm_device_get_configured_mtu_wired_parent;
+    device_class->is_available          = is_available;
+    device_class->link_changed          = link_changed;
+    device_class->parent_changed_notify = parent_changed_notify;
+    device_class->update_connection     = update_connection;
+
+    obj_properties[PROP_MODE] = g_param_spec_string(NM_DEVICE_MACVLAN_MODE,
+                                                    "",
+                                                    "",
+                                                    NULL,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_NO_PROMISC] =
+        g_param_spec_boolean(NM_DEVICE_MACVLAN_NO_PROMISC,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TAP] =
+        g_param_spec_boolean(NM_DEVICE_MACVLAN_TAP,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_MACVLAN_DEVICE_FACTORY (nm_macvlan_device_factory_get_type())
+#define NM_MACVLAN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACVLAN_DEVICE_FACTORY, NMMacvlanDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    NMSettingMacvlan *s_macvlan;
+    NMLinkType        link_type;
+    gboolean          tap;
+
+    if (connection) {
+        s_macvlan = nm_connection_get_setting_macvlan(connection);
+        g_assert(s_macvlan);
+        tap = nm_setting_macvlan_get_tap(s_macvlan);
+    } else {
+        g_assert(plink);
+        tap = plink->type == NM_LINK_TYPE_MACVTAP;
+    }
+
+    link_type = tap ? NM_LINK_TYPE_MACVTAP : NM_LINK_TYPE_MACVLAN;
+
+    return g_object_new(NM_TYPE_DEVICE_MACVLAN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Macvlan",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_MACVLAN,
+                        NM_DEVICE_LINK_TYPE,
+                        link_type,
+                        NM_DEVICE_MACVLAN_TAP,
+                        tap,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingMacvlan *s_macvlan;
+    NMSettingWired *  s_wired;
+    const char *      parent = NULL;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACVLAN_SETTING_NAME), NULL);
+
+    s_macvlan = nm_connection_get_setting_macvlan(connection);
+    g_assert(s_macvlan);
+
+    parent = nm_setting_macvlan_get_parent(s_macvlan);
+    if (parent)
+        return parent;
+
+    /* Try the hardware address from the MACVLAN connection's hardware setting */
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (s_wired)
+        return nm_setting_wired_get_mac_address(s_wired);
+
+    return NULL;
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    NMSettingMacvlan *s_macvlan;
+    const char *      ifname;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_MACVLAN_SETTING_NAME), NULL);
+
+    s_macvlan = nm_connection_get_setting_macvlan(connection);
+    g_assert(s_macvlan);
+
+    if (!parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+    return g_strdup(ifname);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    MACVLAN,
+    Macvlan,
+    macvlan,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_MACVLAN, NM_LINK_TYPE_MACVTAP)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_MACVLAN_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-macvlan.h b/src/core/devices/nm-device-macvlan.h
new file mode 100644
index 00000000..109a2bcd
--- /dev/null
+++ b/src/core/devices/nm-device-macvlan.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_MACVLAN_H__
+#define __NETWORKMANAGER_DEVICE_MACVLAN_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_MACVLAN (nm_device_macvlan_get_type())
+#define NM_DEVICE_MACVLAN(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlan))
+#define NM_DEVICE_MACVLAN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlanClass))
+#define NM_IS_DEVICE_MACVLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MACVLAN))
+#define NM_IS_DEVICE_MACVLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MACVLAN))
+#define NM_DEVICE_MACVLAN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MACVLAN, NMDeviceMacvlanClass))
+
+#define NM_DEVICE_MACVLAN_MODE       "mode"
+#define NM_DEVICE_MACVLAN_NO_PROMISC "no-promisc"
+#define NM_DEVICE_MACVLAN_TAP        "tap"
+
+typedef struct _NMDeviceMacvlan      NMDeviceMacvlan;
+typedef struct _NMDeviceMacvlanClass NMDeviceMacvlanClass;
+
+GType nm_device_macvlan_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_MACVLAN_H__ */
diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c
new file mode 100644
index 00000000..4040f2d3
--- /dev/null
+++ b/src/core/devices/nm-device-ppp.c
@@ -0,0 +1,375 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ppp.h"
+
+#include "nm-ip4-config.h"
+#include "nm-act-request.h"
+#include "nm-device-factory.h"
+#include "nm-device-private.h"
+#include "nm-manager.h"
+#include "nm-setting-pppoe.h"
+#include "platform/nm-platform.h"
+#include "ppp/nm-ppp-manager.h"
+#include "ppp/nm-ppp-manager-call.h"
+#include "ppp/nm-ppp-status.h"
+
+#define _NMLOG_DEVICE_TYPE NMDevicePpp
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+typedef struct _NMDevicePppPrivate {
+    NMPPPManager *ppp_manager;
+    NMIP4Config * ip4_config;
+} NMDevicePppPrivate;
+
+struct _NMDevicePpp {
+    NMDevice           parent;
+    NMDevicePppPrivate _priv;
+};
+
+struct _NMDevicePppClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDevicePpp, nm_device_ppp, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_PPP_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDevicePpp, NM_IS_DEVICE_PPP, NMDevice)
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    switch (status) {
+    case NM_PPP_STATUS_DISCONNECT:
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
+        break;
+    case NM_PPP_STATUS_DEAD:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PPP_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
+{
+    NMDevice *    device        = NM_DEVICE(user_data);
+    NMDevicePpp * self          = NM_DEVICE_PPP(device);
+    gs_free char *old_name      = NULL;
+    gs_free_error GError *error = NULL;
+
+    if (!nm_device_take_over_link(device, ifindex, &old_name, &error)) {
+        _LOGW(LOGD_DEVICE | LOGD_PPP,
+              "could not take control of link %d: %s",
+              ifindex,
+              error->message);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    if (old_name)
+        nm_manager_remove_device(NM_MANAGER_GET, old_name, NM_DEVICE_TYPE_PPP);
+
+    nm_device_activate_schedule_stage3_ip_config_start(device);
+}
+
+static void
+ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
+{
+    NMDevice *          device = NM_DEVICE(user_data);
+    NMDevicePpp *       self   = NM_DEVICE_PPP(device);
+    NMDevicePppPrivate *priv   = NM_DEVICE_PPP_GET_PRIVATE(self);
+
+    _LOGT(LOGD_DEVICE | LOGD_PPP, "received IPv4 config from pppd");
+
+    if (nm_device_get_state(device) == NM_DEVICE_STATE_IP_CONFIG) {
+        if (nm_device_activate_ip4_state_in_conf(device)) {
+            nm_device_activate_schedule_ip_config_result(device,
+                                                         AF_INET,
+                                                         NM_IP_CONFIG_CAST(config));
+            return;
+        }
+    } else {
+        if (priv->ip4_config)
+            g_object_unref(priv->ip4_config);
+        priv->ip4_config = g_object_ref(config);
+    }
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingPppoe *s_pppoe;
+
+    if (!NM_DEVICE_CLASS(nm_device_ppp_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_pppoe = nm_connection_get_setting_pppoe(connection);
+    if (!s_pppoe || !nm_setting_pppoe_get_parent(s_pppoe)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                   "the connection doesn't specify a PPPoE parent interface");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDevicePpp *       self = NM_DEVICE_PPP(device);
+    NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self);
+    NMSettingPppoe *    s_pppoe;
+    NMActRequest *      req;
+    GError *            error = NULL;
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_pppoe = nm_device_get_applied_setting(device, NM_TYPE_SETTING_PPPOE);
+    g_return_val_if_fail(s_pppoe, NM_ACT_STAGE_RETURN_FAILURE);
+
+    g_clear_object(&priv->ip4_config);
+
+    priv->ppp_manager = nm_ppp_manager_create(nm_setting_pppoe_get_parent(s_pppoe), &error);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            nm_device_get_route_table(device, AF_INET),
+                                            nm_device_get_route_metric(device, AF_INET),
+                                            nm_device_get_route_table(device, AF_INET6),
+                                            nm_device_get_route_metric(device, AF_INET6));
+    }
+
+    if (!priv->ppp_manager
+        || !nm_ppp_manager_start(priv->ppp_manager,
+                                 req,
+                                 nm_setting_pppoe_get_username(s_pppoe),
+                                 30,
+                                 0,
+                                 &error)) {
+        _LOGW(LOGD_DEVICE | LOGD_PPP, "PPPoE failed to start: %s", error->message);
+        g_error_free(error);
+
+        g_clear_object(&priv->ppp_manager);
+
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
+                     G_CALLBACK(ppp_state_changed),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
+                     G_CALLBACK(ppp_ifindex_set),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
+                     G_CALLBACK(ppp_ip4_config),
+                     self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    if (addr_family == AF_INET) {
+        NMDevicePpp *       self = NM_DEVICE_PPP(device);
+        NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self);
+
+        if (priv->ip4_config) {
+            if (out_config)
+                *out_config = g_steal_pointer(&priv->ip4_config);
+            else
+                g_clear_object(&priv->ip4_config);
+            return NM_ACT_STAGE_RETURN_SUCCESS;
+        }
+
+        /* Wait IPCP termination */
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    return NM_DEVICE_CLASS(nm_device_ppp_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    int parent_ifindex;
+
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "PPP devices can not be created without a parent interface");
+        return FALSE;
+    }
+
+    parent_ifindex = nm_device_get_ifindex(parent);
+    g_warn_if_fail(parent_ifindex > 0);
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+
+    /* The interface is created later */
+
+    return TRUE;
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDevicePpp *       self = NM_DEVICE_PPP(device);
+    NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
+        g_clear_object(&priv->ppp_manager);
+    }
+}
+
+static void
+nm_device_ppp_init(NMDevicePpp *self)
+{}
+
+static void
+dispose(GObject *object)
+{
+    NMDevicePpp *       self = NM_DEVICE_PPP(object);
+    NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self);
+
+    g_clear_object(&priv->ip4_config);
+
+    G_OBJECT_CLASS(nm_device_ppp_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_ppp = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_PPP,
+        .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ppp_class_init(NMDevicePppClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ppp);
+
+    device_class->connection_type_supported        = NM_SETTING_PPPOE_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_PPPOE_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_PPP);
+
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->act_stage3_ip_config_start  = act_stage3_ip_config_start;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->deactivate                  = deactivate;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_PPP_DEVICE_FACTORY (nm_ppp_device_factory_get_type())
+#define NM_PPP_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_DEVICE_FACTORY, NMPppDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_PPP,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Ppp",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_PPP,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_PPP,
+                        NULL);
+}
+
+static gboolean
+match_connection(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingPppoe *s_pppoe;
+
+    s_pppoe = nm_connection_get_setting_pppoe(connection);
+    nm_assert(s_pppoe);
+
+    return !!nm_setting_pppoe_get_parent(s_pppoe);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingPppoe *s_pppoe;
+
+    nm_assert(nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME));
+
+    s_pppoe = nm_connection_get_setting_pppoe(connection);
+    nm_assert(s_pppoe);
+
+    return nm_setting_pppoe_get_parent(s_pppoe);
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    nm_assert(nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME));
+
+    if (!parent_iface)
+        return NULL;
+
+    return g_strdup(nm_connection_get_interface_name(connection));
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    PPP,
+    Ppp,
+    ppp,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_PPP)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_PPPOE_SETTING_NAME),
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;
+    factory_class->create_device         = create_device;
+    factory_class->match_connection      = match_connection;);
diff --git a/src/core/devices/nm-device-ppp.h b/src/core/devices/nm-device-ppp.h
new file mode 100644
index 00000000..24c119ab
--- /dev/null
+++ b/src/core/devices/nm-device-ppp.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_PPP_H__
+#define __NETWORKMANAGER_DEVICE_PPP_H__
+
+#define NM_TYPE_DEVICE_PPP (nm_device_ppp_get_type())
+#define NM_DEVICE_PPP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_PPP, NMDevicePpp))
+#define NM_DEVICE_PPP_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_PPP, NMDevicePppClass))
+#define NM_IS_DEVICE_PPP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_PPP))
+#define NM_IS_DEVICE_PPP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_PPP))
+#define NM_DEVICE_PPP_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_PPP, NMDevicePppClass))
+
+typedef struct _NMDevicePpp      NMDevicePpp;
+typedef struct _NMDevicePppClass NMDevicePppClass;
+
+GType nm_device_ppp_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_PPP_H__ */
diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
new file mode 100644
index 00000000..8675a699
--- /dev/null
+++ b/src/core/devices/nm-device-private.h
@@ -0,0 +1,213 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2007 - 2008 Novell, Inc.
+ * Copyright (C) 2007 - 2011 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_PRIVATE_H__
+#define __NETWORKMANAGER_DEVICE_PRIVATE_H__
+
+#include "nm-device.h"
+
+/* This file should only be used by subclasses of NMDevice */
+
+typedef enum {
+    NM_DEVICE_STAGE_STATE_INIT      = 0,
+    NM_DEVICE_STAGE_STATE_PENDING   = 1,
+    NM_DEVICE_STAGE_STATE_COMPLETED = 2,
+} NMDeviceStageState;
+
+typedef enum {
+    NM_DEVICE_IP_STATE_NONE,
+    NM_DEVICE_IP_STATE_WAIT,
+    NM_DEVICE_IP_STATE_CONF,
+    NM_DEVICE_IP_STATE_DONE,
+    NM_DEVICE_IP_STATE_FAIL,
+} NMDeviceIPState;
+
+enum NMActStageReturn {
+    NM_ACT_STAGE_RETURN_FAILURE = 0, /* Hard failure of activation */
+    NM_ACT_STAGE_RETURN_SUCCESS,     /* Activation stage done */
+    NM_ACT_STAGE_RETURN_POSTPONE,    /* Long-running operation in progress */
+    NM_ACT_STAGE_RETURN_IP_WAIT,     /* IP config stage is waiting (state IP_WAIT) */
+    NM_ACT_STAGE_RETURN_IP_DONE,     /* IP config stage is done (state IP_DONE),
+                                      * For the ip-config stage, this is similar to
+                                      * NM_ACT_STAGE_RETURN_SUCCESS, except that no
+                                      * IP config should be committed. */
+    NM_ACT_STAGE_RETURN_IP_FAIL, /* IP config stage failed (state IP_FAIL), activation may proceed */
+};
+
+#define NM_DEVICE_CAP_NONSTANDARD_CARRIER 0x80000000
+#define NM_DEVICE_CAP_IS_NON_KERNEL       0x40000000
+
+#define NM_DEVICE_CAP_INTERNAL_MASK 0xc0000000
+
+void nm_device_arp_announce(NMDevice *self);
+
+NMSettings *nm_device_get_settings(NMDevice *self);
+
+NMManager *nm_device_get_manager(NMDevice *self);
+
+gboolean nm_device_set_ip_ifindex(NMDevice *self, int ifindex);
+
+gboolean nm_device_set_ip_iface(NMDevice *self, const char *iface);
+
+void nm_device_activate_schedule_stage3_ip_config_start(NMDevice *device);
+
+gboolean nm_device_activate_stage3_ip_start(NMDevice *self, int addr_family);
+
+gboolean nm_device_bring_up(NMDevice *self, gboolean wait, gboolean *no_firmware);
+
+void nm_device_take_down(NMDevice *self, gboolean block);
+
+gboolean nm_device_take_over_link(NMDevice *self, int ifindex, char **old_name, GError **error);
+
+gboolean nm_device_hw_addr_set(NMDevice *  device,
+                               const char *addr,
+                               const char *detail,
+                               gboolean    set_permanent);
+gboolean nm_device_hw_addr_set_cloned(NMDevice *device, NMConnection *connection, gboolean is_wifi);
+gboolean nm_device_hw_addr_reset(NMDevice *device, const char *detail);
+
+void nm_device_set_firmware_missing(NMDevice *self, gboolean missing);
+
+void nm_device_activate_schedule_stage1_device_prepare(NMDevice *device, gboolean do_sync);
+void nm_device_activate_schedule_stage2_device_config(NMDevice *device, gboolean do_sync);
+
+void
+nm_device_activate_schedule_ip_config_result(NMDevice *device, int addr_family, NMIPConfig *config);
+
+void nm_device_activate_schedule_ip_config_timeout(NMDevice *device, int addr_family);
+
+NMDeviceIPState nm_device_activate_get_ip_state(NMDevice *self, int addr_family);
+
+static inline gboolean
+nm_device_activate_ip4_state_in_conf(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_CONF;
+}
+
+static inline gboolean
+nm_device_activate_ip4_state_in_wait(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_WAIT;
+}
+
+static inline gboolean
+nm_device_activate_ip4_state_done(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET) == NM_DEVICE_IP_STATE_DONE;
+}
+
+static inline gboolean
+nm_device_activate_ip6_state_in_conf(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_CONF;
+}
+
+static inline gboolean
+nm_device_activate_ip6_state_in_wait(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_WAIT;
+}
+
+static inline gboolean
+nm_device_activate_ip6_state_done(NMDevice *self)
+{
+    return nm_device_activate_get_ip_state(self, AF_INET6) == NM_DEVICE_IP_STATE_DONE;
+}
+
+void nm_device_set_dhcp_anycast_address(NMDevice *device, const char *addr);
+
+gboolean nm_device_dhcp4_renew(NMDevice *device, gboolean release);
+gboolean nm_device_dhcp6_renew(NMDevice *device, gboolean release);
+
+void nm_device_recheck_available_connections(NMDevice *device);
+
+void
+nm_device_master_check_slave_physical_port(NMDevice *self, NMDevice *slave, NMLogDomain log_domain);
+
+void nm_device_master_release_slaves(NMDevice *self);
+
+void nm_device_set_carrier(NMDevice *self, gboolean carrier);
+
+void nm_device_queue_recheck_assume(NMDevice *device);
+void nm_device_queue_recheck_available(NMDevice *          device,
+                                       NMDeviceStateReason available_reason,
+                                       NMDeviceStateReason unavailable_reason);
+
+void nm_device_set_dev2_ip_config(NMDevice *device, int addr_family, NMIPConfig *config);
+
+gboolean nm_device_hw_addr_is_explict(NMDevice *device);
+
+void nm_device_ip_method_failed(NMDevice *self, int addr_family, NMDeviceStateReason reason);
+
+gboolean nm_device_sysctl_ip_conf_set(NMDevice *  self,
+                                      int         addr_family,
+                                      const char *property,
+                                      const char *value);
+
+NMIP4Config *nm_device_ip4_config_new(NMDevice *self);
+
+NMIP6Config *nm_device_ip6_config_new(NMDevice *self);
+
+NMIPConfig *nm_device_ip_config_new(NMDevice *self, int addr_family);
+
+NML3ConfigData *nm_device_create_l3_config_data(NMDevice *self);
+
+/*****************************************************************************/
+
+gint64 nm_device_get_configured_mtu_from_connection_default(NMDevice *  self,
+                                                            const char *property_name,
+                                                            guint32     max_mtu);
+
+guint32 nm_device_get_configured_mtu_from_connection(NMDevice *         device,
+                                                     GType              setting_type,
+                                                     NMDeviceMtuSource *out_source);
+
+guint32 nm_device_get_configured_mtu_for_wired(NMDevice *         self,
+                                               NMDeviceMtuSource *out_source,
+                                               gboolean *         out_force);
+
+guint32 nm_device_get_configured_mtu_wired_parent(NMDevice *         self,
+                                                  NMDeviceMtuSource *out_source,
+                                                  gboolean *         out_force);
+
+void nm_device_commit_mtu(NMDevice *self);
+
+/*****************************************************************************/
+
+#define NM_DEVICE_DEFINE_LINK_TYPES(...)                                        \
+    ((NM_NARG(__VA_ARGS__) == 0) ? NULL : ({                                    \
+        static const NMLinkType _types[NM_NARG(__VA_ARGS__) + 1] = {            \
+            __VA_ARGS__ _NM_MACRO_COMMA_IF_ARGS(__VA_ARGS__) NM_LINK_TYPE_NONE, \
+        };                                                                      \
+                                                                                \
+        nm_assert(_types[NM_NARG(__VA_ARGS__)] == NM_LINK_TYPE_NONE);           \
+        _types;                                                                 \
+    }))
+
+gboolean _nm_device_hash_check_invalid_keys(GHashTable *       hash,
+                                            const char *       setting_name,
+                                            GError **          error,
+                                            const char *const *whitelist);
+#define nm_device_hash_check_invalid_keys(hash, setting_name, error, ...) \
+    _nm_device_hash_check_invalid_keys(hash, setting_name, error, NM_MAKE_STRV(__VA_ARGS__))
+
+gboolean nm_device_match_parent(NMDevice *device, const char *parent);
+gboolean nm_device_match_parent_hwaddr(NMDevice *    device,
+                                       NMConnection *connection,
+                                       gboolean      fail_if_no_hwaddr);
+
+/*****************************************************************************/
+
+void nm_device_auth_request(NMDevice *                     self,
+                            GDBusMethodInvocation *        context,
+                            NMConnection *                 connection,
+                            const char *                   permission,
+                            gboolean                       allow_interaction,
+                            GCancellable *                 cancellable,
+                            NMManagerDeviceAuthRequestFunc callback,
+                            gpointer                       user_data);
+
+#endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c
new file mode 100644
index 00000000..edca69e9
--- /dev/null
+++ b/src/core/devices/nm-device-tun.c
@@ -0,0 +1,571 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-tun.h"
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <linux/if_tun.h>
+
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-tun.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceTun
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceTun,
+                             PROP_OWNER,
+                             PROP_GROUP,
+                             PROP_MODE,
+                             PROP_NO_PI,
+                             PROP_VNET_HDR,
+                             PROP_MULTI_QUEUE, );
+
+typedef struct {
+    NMPlatformLnkTun props;
+} NMDeviceTunPrivate;
+
+struct _NMDeviceTun {
+    NMDevice           parent;
+    NMDeviceTunPrivate _priv;
+};
+
+struct _NMDeviceTunClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceTun, nm_device_tun, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_TUN_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceTun, NM_IS_DEVICE_TUN, NMDevice)
+
+/*****************************************************************************/
+
+static void
+update_properties_from_struct(NMDeviceTun *self, const NMPlatformLnkTun *props)
+{
+    NMDeviceTunPrivate *   priv   = NM_DEVICE_TUN_GET_PRIVATE(self);
+    const NMPlatformLnkTun props0 = {};
+
+    if (!props) {
+        /* allow passing %NULL to reset all properties. */
+        props = &props0;
+    }
+
+    g_object_freeze_notify(G_OBJECT(self));
+
+#define CHECK_PROPERTY_CHANGED_VALID(field, prop)                   \
+    G_STMT_START                                                    \
+    {                                                               \
+        if (priv->props.field != props->field                       \
+            || priv->props.field##_valid != props->field##_valid) { \
+            priv->props.field##_valid = props->field##_valid;       \
+            priv->props.field         = props->field;               \
+            _notify(self, prop);                                    \
+        }                                                           \
+    }                                                               \
+    G_STMT_END
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED_VALID(owner, PROP_OWNER);
+    CHECK_PROPERTY_CHANGED_VALID(group, PROP_GROUP);
+    CHECK_PROPERTY_CHANGED(type, PROP_MODE);
+    CHECK_PROPERTY_CHANGED(pi, PROP_NO_PI);
+    CHECK_PROPERTY_CHANGED(vnet_hdr, PROP_VNET_HDR);
+    CHECK_PROPERTY_CHANGED(multi_queue, PROP_MULTI_QUEUE);
+
+    g_object_thaw_notify(G_OBJECT(self));
+}
+
+static void
+update_properties(NMDeviceTun *self)
+{
+    NMPlatformLnkTun        props_storage;
+    const NMPlatformLnkTun *props = NULL;
+    int                     ifindex;
+
+    ifindex = nm_device_get_ifindex(NM_DEVICE(self));
+    if (ifindex > 0
+        && nm_platform_link_tun_get_properties(nm_device_get_platform(NM_DEVICE(self)),
+                                               ifindex,
+                                               &props_storage))
+        props = &props_storage;
+
+    update_properties_from_struct(self, props);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_tun_parent_class)->link_changed(device, pllink);
+    update_properties(NM_DEVICE_TUN(device));
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingTun *s_tun;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_TUN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("TUN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_tun = nm_connection_get_setting_tun(connection);
+    if (!s_tun) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'tun' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceTun *       self = NM_DEVICE_TUN(device);
+    NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self);
+    NMSettingTun *      s_tun;
+    NMSettingTunMode    mode;
+    char                s_buf[100];
+    const char *        str;
+
+    /* Note: since we read tun properties from sysctl for older kernels,
+     *       we don't get proper change notifications. Make sure that all our
+     *       tun properties are up to date at this point. We should not do this,
+     *       if we would entirely rely on netlink events. */
+    update_properties(NM_DEVICE_TUN(device));
+
+    switch (priv->props.type) {
+    case IFF_TUN:
+        mode = NM_SETTING_TUN_MODE_TUN;
+        break;
+    case IFF_TAP:
+        mode = NM_SETTING_TUN_MODE_TAP;
+        break;
+    default:
+        /* Huh? */
+        return;
+    }
+
+    s_tun = nm_connection_get_setting_tun(connection);
+    if (!s_tun) {
+        s_tun = (NMSettingTun *) nm_setting_tun_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_tun);
+    }
+
+    if (mode != nm_setting_tun_get_mode(s_tun))
+        g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_MODE, (guint) mode, NULL);
+
+    str = priv->props.owner_valid ? nm_sprintf_buf(s_buf, "%" G_GINT32_FORMAT, priv->props.owner)
+                                  : NULL;
+    if (!nm_streq0(str, nm_setting_tun_get_owner(s_tun)))
+        g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_OWNER, str, NULL);
+
+    str = priv->props.group_valid ? nm_sprintf_buf(s_buf, "%" G_GINT32_FORMAT, priv->props.group)
+                                  : NULL;
+    if (!nm_streq0(str, nm_setting_tun_get_group(s_tun)))
+        g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_GROUP, str, NULL);
+
+    if (priv->props.pi != nm_setting_tun_get_pi(s_tun))
+        g_object_set(G_OBJECT(s_tun), NM_SETTING_TUN_PI, (gboolean) priv->props.pi, NULL);
+    if (priv->props.vnet_hdr != nm_setting_tun_get_vnet_hdr(s_tun))
+        g_object_set(G_OBJECT(s_tun),
+                     NM_SETTING_TUN_VNET_HDR,
+                     (gboolean) priv->props.vnet_hdr,
+                     NULL);
+    if (priv->props.multi_queue != nm_setting_tun_get_multi_queue(s_tun))
+        g_object_set(G_OBJECT(s_tun),
+                     NM_SETTING_TUN_MULTI_QUEUE,
+                     (gboolean) priv->props.multi_queue,
+                     NULL);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *     iface = nm_device_get_iface(device);
+    NMPlatformLnkTun props = {};
+    NMSettingTun *   s_tun;
+    gint64           owner;
+    gint64           group;
+    int              r;
+
+    s_tun = nm_connection_get_setting_tun(connection);
+    g_return_val_if_fail(s_tun, FALSE);
+
+    switch (nm_setting_tun_get_mode(s_tun)) {
+    case NM_SETTING_TUN_MODE_TAP:
+        props.type = IFF_TAP;
+        break;
+    case NM_SETTING_TUN_MODE_TUN:
+        props.type = IFF_TUN;
+        break;
+    default:
+        g_return_val_if_reached(FALSE);
+    }
+
+    owner = _nm_utils_ascii_str_to_int64(nm_setting_tun_get_owner(s_tun), 10, 0, G_MAXINT32, -1);
+    if (owner != -1) {
+        props.owner_valid = TRUE;
+        props.owner       = owner;
+    }
+    group = _nm_utils_ascii_str_to_int64(nm_setting_tun_get_group(s_tun), 10, 0, G_MAXINT32, -1);
+    if (group != -1) {
+        props.group_valid = TRUE;
+        props.group       = group;
+    }
+
+    props.pi          = nm_setting_tun_get_pi(s_tun);
+    props.vnet_hdr    = nm_setting_tun_get_vnet_hdr(s_tun);
+    props.multi_queue = nm_setting_tun_get_multi_queue(s_tun);
+    props.persist     = TRUE;
+
+    r = nm_platform_link_tun_add(nm_device_get_platform(device), iface, &props, out_plink, NULL);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create TUN/TAP interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_same_og(const char *str, gboolean og_valid, guint32 og_num)
+{
+    gint64 v;
+
+    v = _nm_utils_ascii_str_to_int64(str, 10, 0, G_MAXINT32, -1);
+    return (!og_valid && (v == (gint64) -1)) || (og_valid && (((guint32) v) == og_num));
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceTun *       self = NM_DEVICE_TUN(device);
+    NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self);
+    NMSettingTunMode    mode;
+    NMSettingTun *      s_tun;
+
+    if (!NM_DEVICE_CLASS(nm_device_tun_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_device_is_real(device)) {
+        switch (priv->props.type) {
+        case IFF_TUN:
+            mode = NM_SETTING_TUN_MODE_TUN;
+            break;
+        case IFF_TAP:
+            mode = NM_SETTING_TUN_MODE_TAP;
+            break;
+        default:
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "invalid tun type on device");
+            return FALSE;
+        }
+
+        s_tun = nm_connection_get_setting_tun(connection);
+
+        if (mode != nm_setting_tun_get_mode(s_tun)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun mode setting mismatches");
+            return FALSE;
+        }
+        if (!_same_og(nm_setting_tun_get_owner(s_tun),
+                      priv->props.owner_valid,
+                      priv->props.owner)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun owner setting mismatches");
+            return FALSE;
+        }
+        if (!_same_og(nm_setting_tun_get_group(s_tun),
+                      priv->props.group_valid,
+                      priv->props.group)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun group setting mismatches");
+            return FALSE;
+        }
+        if (nm_setting_tun_get_pi(s_tun) != priv->props.pi) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun pi setting mismatches");
+            return FALSE;
+        }
+        if (nm_setting_tun_get_vnet_hdr(s_tun) != priv->props.vnet_hdr) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun vnet-hdr setting mismatches");
+            return FALSE;
+        }
+        if (nm_setting_tun_get_multi_queue(s_tun) != priv->props.multi_queue) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "tun multi-queue setting mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceTun *       self = NM_DEVICE_TUN(device);
+    NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self);
+
+    if (priv->props.type == IFF_TUN) {
+        /* Nothing to do for TUN devices */
+    } else {
+        if (!nm_device_hw_addr_set_cloned(device,
+                                          nm_device_get_applied_connection(device),
+                                          FALSE)) {
+            *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED;
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NM_DEVICE_CLASS(nm_device_tun_parent_class)->unrealize_notify(device);
+    update_properties_from_struct(NM_DEVICE_TUN(device), NULL);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceTun *       self = NM_DEVICE_TUN(object);
+    NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self);
+    const char *        s;
+
+    switch (prop_id) {
+    case PROP_OWNER:
+        g_value_set_int64(value,
+                          priv->props.owner_valid ? (gint64) priv->props.owner : (gint64) -1);
+        break;
+    case PROP_GROUP:
+        g_value_set_int64(value,
+                          priv->props.group_valid ? (gint64) priv->props.group : (gint64) -1);
+        break;
+    case PROP_MODE:
+        switch (priv->props.type) {
+        case IFF_TUN:
+            s = "tun";
+            break;
+        case IFF_TAP:
+            s = "tap";
+            break;
+        default:
+            s = NULL;
+            break;
+        }
+        g_value_set_static_string(value, s);
+        break;
+    case PROP_NO_PI:
+        g_value_set_boolean(value, !priv->props.pi);
+        break;
+    case PROP_VNET_HDR:
+        g_value_set_boolean(value, priv->props.vnet_hdr);
+        break;
+    case PROP_MULTI_QUEUE:
+        g_value_set_boolean(value, priv->props.multi_queue);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_tun_init(NMDeviceTun *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_tun = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_TUN,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Owner", "x", NM_DEVICE_TUN_OWNER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Group", "x", NM_DEVICE_TUN_GROUP),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "s", NM_DEVICE_TUN_MODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NoPi", "b", NM_DEVICE_TUN_NO_PI),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("VnetHdr",
+                                                             "b",
+                                                             NM_DEVICE_TUN_VNET_HDR),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("MultiQueue",
+                                                             "b",
+                                                             NM_DEVICE_TUN_MULTI_QUEUE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_tun_class_init(NMDeviceTunClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_tun);
+
+    device_class->connection_type_supported        = NM_SETTING_TUN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_TUN_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_TUN);
+
+    device_class->link_changed                = link_changed;
+    device_class->complete_connection         = complete_connection;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->unrealize_notify            = unrealize_notify;
+    device_class->update_connection           = update_connection;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->get_configured_mtu          = nm_device_get_configured_mtu_for_wired;
+
+    obj_properties[PROP_OWNER] = g_param_spec_int64(NM_DEVICE_TUN_OWNER,
+                                                    "",
+                                                    "",
+                                                    -1,
+                                                    G_MAXUINT32,
+                                                    -1,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_GROUP] = g_param_spec_int64(NM_DEVICE_TUN_GROUP,
+                                                    "",
+                                                    "",
+                                                    -1,
+                                                    G_MAXUINT32,
+                                                    -1,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MODE] = g_param_spec_string(NM_DEVICE_TUN_MODE,
+                                                    "",
+                                                    "",
+                                                    NULL,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_NO_PI] = g_param_spec_boolean(NM_DEVICE_TUN_NO_PI,
+                                                      "",
+                                                      "",
+                                                      FALSE,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_VNET_HDR] = g_param_spec_boolean(NM_DEVICE_TUN_VNET_HDR,
+                                                         "",
+                                                         "",
+                                                         FALSE,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MULTI_QUEUE] =
+        g_param_spec_boolean(NM_DEVICE_TUN_MULTI_QUEUE,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_TUN_DEVICE_FACTORY (nm_tun_device_factory_get_type())
+#define NM_TUN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TUN_DEVICE_FACTORY, NMTunDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    g_return_val_if_fail(!plink || plink->type == NM_LINK_TYPE_TUN, NULL);
+    g_return_val_if_fail(!connection
+                             || nm_streq0(nm_connection_get_connection_type(connection),
+                                          NM_SETTING_TUN_SETTING_NAME),
+                         NULL);
+
+    return g_object_new(NM_TYPE_DEVICE_TUN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Tun",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_TUN,
+                        NM_DEVICE_LINK_TYPE,
+                        (guint) NM_LINK_TYPE_TUN,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    TUN,
+    Tun,
+    tun,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_TUN)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_TUN_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-tun.h b/src/core/devices/nm-device-tun.h
new file mode 100644
index 00000000..84497ea0
--- /dev/null
+++ b/src/core/devices/nm-device-tun.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_TUN_H__
+#define __NETWORKMANAGER_DEVICE_TUN_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_TUN (nm_device_tun_get_type())
+#define NM_DEVICE_TUN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TUN, NMDeviceTun))
+#define NM_DEVICE_TUN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TUN, NMDeviceTunClass))
+#define NM_IS_DEVICE_TUN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TUN))
+#define NM_IS_DEVICE_TUN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_TUN))
+#define NM_DEVICE_TUN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_TUN, NMDeviceTunClass))
+
+#define NM_DEVICE_TUN_OWNER       "owner"
+#define NM_DEVICE_TUN_GROUP       "group"
+#define NM_DEVICE_TUN_MODE        "mode"
+#define NM_DEVICE_TUN_NO_PI       "no-pi"
+#define NM_DEVICE_TUN_VNET_HDR    "vnet-hdr"
+#define NM_DEVICE_TUN_MULTI_QUEUE "multi-queue"
+
+typedef struct _NMDeviceTun      NMDeviceTun;
+typedef struct _NMDeviceTunClass NMDeviceTunClass;
+
+GType nm_device_tun_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_TUN_H__ */
diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c
new file mode 100644
index 00000000..e0ba843d
--- /dev/null
+++ b/src/core/devices/nm-device-veth.c
@@ -0,0 +1,232 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <stdlib.h>
+
+#include "nm-core-internal.h"
+#include "nm-device-veth.h"
+#include "nm-device-private.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-veth.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceVeth
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceVeth {
+    NMDeviceEthernet parent;
+};
+
+struct _NMDeviceVethClass {
+    NMDeviceEthernetClass parent;
+};
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVeth, PROP_PEER, );
+
+/*****************************************************************************/
+
+G_DEFINE_TYPE(NMDeviceVeth, nm_device_veth, NM_TYPE_DEVICE_ETHERNET)
+
+/*****************************************************************************/
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDevice *peer;
+    int       ifindex, peer_ifindex;
+
+    ifindex = nm_device_get_ifindex(device);
+
+    if (ifindex <= 0
+        || !nm_platform_link_veth_get_properties(nm_device_get_platform(device),
+                                                 ifindex,
+                                                 &peer_ifindex))
+        peer_ifindex = 0;
+
+    nm_device_parent_set_ifindex(device, peer_ifindex);
+
+    peer = nm_device_parent_get_device(device);
+    if (peer && NM_IS_DEVICE_VETH(peer) && nm_device_parent_get_ifindex(peer) <= 0)
+        update_properties(peer);
+}
+
+static gboolean
+can_unmanaged_external_down(NMDevice *self)
+{
+    /* Unless running in a container, an udev rule causes these to be
+     * unmanaged. If there's no udev then we're probably in a container
+     * and should IFF_UP and configure the veth ourselves even if we
+     * didn't create it. */
+    return FALSE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_veth_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *   iface = nm_device_get_iface(device);
+    NMSettingVeth *s_veth;
+    int            r;
+
+    s_veth = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VETH);
+    if (!s_veth) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Profile %s (%s) is not a suitable veth profile",
+                    nm_connection_get_id(connection),
+                    nm_connection_get_uuid(connection));
+        return FALSE;
+    }
+
+    r = nm_platform_link_veth_add(nm_device_get_platform(device),
+                                  iface,
+                                  nm_setting_veth_get_peer(s_veth),
+                                  out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create veth interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_veth_init(NMDeviceVeth *self)
+{}
+
+static void
+parent_changed_notify(NMDevice *device,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    NM_DEVICE_CLASS(nm_device_veth_parent_class)
+        ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
+    _notify(NM_DEVICE_VETH(device), PROP_PEER);
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceVeth *self = NM_DEVICE_VETH(object);
+    NMDevice *    peer;
+
+    switch (prop_id) {
+    case PROP_PEER:
+        peer = nm_device_parent_get_device(NM_DEVICE(self));
+        if (peer && !NM_IS_DEVICE_VETH(peer))
+            peer = NULL;
+        nm_dbus_utils_g_value_set_object_path(value, peer);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_veth = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_VETH,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Peer",
+                                                             "o",
+                                                             NM_DEVICE_VETH_PEER), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_veth_class_init(NMDeviceVethClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_veth);
+
+    device_class->connection_type_supported = NULL;
+    device_class->link_types                = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VETH);
+
+    device_class->can_unmanaged_external_down = can_unmanaged_external_down;
+    device_class->link_changed                = link_changed;
+    device_class->parent_changed_notify       = parent_changed_notify;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+
+    obj_properties[PROP_PEER] = g_param_spec_string(NM_DEVICE_VETH_PEER,
+                                                    "",
+                                                    "",
+                                                    NULL,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_VETH_DEVICE_FACTORY (nm_veth_device_factory_get_type())
+#define NM_VETH_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VETH_DEVICE_FACTORY, NMVethDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_VETH,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Veth",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_VETH,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_VETH,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    VETH,
+    Veth,
+    veth,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VETH)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VETH_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-veth.h b/src/core/devices/nm-device-veth.h
new file mode 100644
index 00000000..d43a0a4b
--- /dev/null
+++ b/src/core/devices/nm-device-veth.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_VETH_H__
+#define __NETWORKMANAGER_DEVICE_VETH_H__
+
+#include "nm-device-ethernet.h"
+
+#define NM_TYPE_DEVICE_VETH (nm_device_veth_get_type())
+#define NM_DEVICE_VETH(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VETH, NMDeviceVeth))
+#define NM_DEVICE_VETH_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VETH, NMDeviceVethClass))
+#define NM_IS_DEVICE_VETH(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VETH))
+#define NM_IS_DEVICE_VETH_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VETH))
+#define NM_DEVICE_VETH_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VETH, NMDeviceVethClass))
+
+#define NM_DEVICE_VETH_PEER "peer"
+
+typedef struct _NMDeviceVeth      NMDeviceVeth;
+typedef struct _NMDeviceVethClass NMDeviceVethClass;
+
+GType nm_device_veth_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_VETH_H__ */
diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c
new file mode 100644
index 00000000..bfde60ef
--- /dev/null
+++ b/src/core/devices/nm-device-vlan.c
@@ -0,0 +1,688 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2012 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-vlan.h"
+
+#include <sys/socket.h>
+
+#include "nm-manager.h"
+#include "nm-utils.h"
+#include "NetworkManagerUtils.h"
+#include "nm-device-private.h"
+#include "settings/nm-settings.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-manager.h"
+#include "nm-core-internal.h"
+#include "platform/nmp-object.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceVlan
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVlan, PROP_VLAN_ID, );
+
+typedef struct {
+    gulong parent_state_id;
+    gulong parent_hwaddr_id;
+    gulong parent_mtu_id;
+    guint  vlan_id;
+} NMDeviceVlanPrivate;
+
+struct _NMDeviceVlan {
+    NMDevice            parent;
+    NMDeviceVlanPrivate _priv;
+};
+
+struct _NMDeviceVlanClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceVlan, nm_device_vlan, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_VLAN_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceVlan, NM_IS_DEVICE_VLAN, NMDevice)
+
+/*****************************************************************************/
+
+static void
+parent_state_changed(NMDevice *          parent,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason,
+                     gpointer            user_data)
+{
+    NMDeviceVlan *self = NM_DEVICE_VLAN(user_data);
+
+    /* We'll react to our own carrier state notifications. Ignore the parent's. */
+    if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_CARRIER)
+        return;
+
+    nm_device_set_unmanaged_by_flags(NM_DEVICE(self),
+                                     NM_UNMANAGED_PARENT,
+                                     !nm_device_get_managed(parent, FALSE),
+                                     reason);
+}
+
+static void
+parent_mtu_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
+{
+    /* the MTU of a VLAN device is limited by the parent's MTU.
+     *
+     * When the parent's MTU changes, try to re-set the MTU. */
+    nm_device_commit_mtu(user_data);
+}
+
+static void
+parent_hwaddr_maybe_changed(NMDevice *parent, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *         device = NM_DEVICE(user_data);
+    NMDeviceVlan *     self   = NM_DEVICE_VLAN(device);
+    NMConnection *     connection;
+    const char *       new_mac, *old_mac;
+    NMSettingIPConfig *s_ip6;
+
+    /* Never touch assumed devices */
+    if (nm_device_sys_iface_state_is_external_or_assume(device))
+        return;
+
+    connection = nm_device_get_applied_connection(device);
+    if (!connection)
+        return;
+
+    /* Update the VLAN MAC only if configuration does not specify one */
+    if (nm_device_hw_addr_is_explict(device))
+        return;
+
+    old_mac = nm_device_get_hw_address(device);
+    new_mac = nm_device_get_hw_address(parent);
+    if (nm_streq0(old_mac, new_mac))
+        return;
+
+    _LOGD(LOGD_VLAN,
+          "parent hardware address changed to %s%s%s",
+          NM_PRINT_FMT_QUOTE_STRING(new_mac));
+    if (new_mac) {
+        nm_device_hw_addr_set(device, new_mac, "vlan-parent", TRUE);
+        nm_device_arp_announce(device);
+        /* When changing the hw address the interface is taken down,
+         * removing the IPv6 configuration; reapply it.
+         */
+        s_ip6 = nm_connection_get_setting_ip6_config(connection);
+        if (s_ip6)
+            nm_device_reactivate_ip_config(device, AF_INET6, s_ip6, s_ip6);
+    }
+}
+
+static void
+parent_changed_notify(NMDevice *device,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    NMDeviceVlan *       self = NM_DEVICE_VLAN(device);
+    NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(self);
+
+    NM_DEVICE_CLASS(nm_device_vlan_parent_class)
+        ->parent_changed_notify(device, old_ifindex, old_parent, new_ifindex, new_parent);
+
+    /*  note that @self doesn't have to clear @parent_state_id on dispose,
+     *  because NMDevice's dispose() will unset the parent, which in turn calls
+     *  parent_changed_notify(). */
+    nm_clear_g_signal_handler(old_parent, &priv->parent_state_id);
+    nm_clear_g_signal_handler(old_parent, &priv->parent_hwaddr_id);
+    nm_clear_g_signal_handler(old_parent, &priv->parent_mtu_id);
+
+    if (new_parent) {
+        priv->parent_state_id = g_signal_connect(new_parent,
+                                                 NM_DEVICE_STATE_CHANGED,
+                                                 G_CALLBACK(parent_state_changed),
+                                                 device);
+
+        priv->parent_hwaddr_id = g_signal_connect(new_parent,
+                                                  "notify::" NM_DEVICE_HW_ADDRESS,
+                                                  G_CALLBACK(parent_hwaddr_maybe_changed),
+                                                  device);
+        parent_hwaddr_maybe_changed(new_parent, NULL, self);
+
+        priv->parent_mtu_id = g_signal_connect(new_parent,
+                                               "notify::" NM_DEVICE_MTU,
+                                               G_CALLBACK(parent_mtu_maybe_changed),
+                                               device);
+        parent_mtu_maybe_changed(new_parent, NULL, self);
+
+        /* Set parent-dependent unmanaged flag */
+        nm_device_set_unmanaged_by_flags(device,
+                                         NM_UNMANAGED_PARENT,
+                                         !nm_device_get_managed(new_parent, FALSE),
+                                         NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED);
+    }
+
+    /* Recheck availability now that the parent has changed */
+    if (new_ifindex > 0) {
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED,
+                                          NM_DEVICE_STATE_REASON_PARENT_CHANGED);
+    }
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceVlanPrivate *    priv;
+    const NMPlatformLink *   plink = NULL;
+    const NMPlatformLnkVlan *plnk  = NULL;
+    int                      ifindex;
+    int                      parent_ifindex = 0;
+    guint                    vlan_id;
+
+    g_return_if_fail(NM_IS_DEVICE_VLAN(device));
+
+    priv = NM_DEVICE_VLAN_GET_PRIVATE(device);
+
+    ifindex = nm_device_get_ifindex(device);
+
+    if (ifindex > 0)
+        plnk = nm_platform_link_get_lnk_vlan(nm_device_get_platform(device), ifindex, &plink);
+
+    if (plnk && plink->parent > 0)
+        parent_ifindex = plink->parent;
+
+    g_object_freeze_notify((GObject *) device);
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+
+    vlan_id = plnk ? plnk->id : 0;
+    if (vlan_id != priv->vlan_id) {
+        priv->vlan_id = vlan_id;
+        _notify((NMDeviceVlan *) device, PROP_VLAN_ID);
+    }
+
+    g_object_thaw_notify((GObject *) device);
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_vlan_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    NMDeviceVlanPrivate *priv  = NM_DEVICE_VLAN_GET_PRIVATE(device);
+    const char *         iface = nm_device_get_iface(device);
+    NMSettingVlan *      s_vlan;
+    int                  parent_ifindex;
+    guint                vlan_id;
+    int                  r;
+
+    s_vlan = nm_connection_get_setting_vlan(connection);
+    g_assert(s_vlan);
+
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "VLAN devices can not be created without a parent interface");
+        return FALSE;
+    }
+
+    parent_ifindex = nm_device_get_ifindex(parent);
+    if (parent_ifindex <= 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "cannot retrieve ifindex of interface %s (%s)",
+                    nm_device_get_iface(parent),
+                    nm_device_get_type_desc(parent));
+        return FALSE;
+    }
+
+    if (!nm_device_supports_vlans(parent)) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "no support for VLANs on interface %s of type %s",
+                    nm_device_get_iface(parent),
+                    nm_device_get_type_desc(parent));
+        return FALSE;
+    }
+
+    vlan_id = nm_setting_vlan_get_id(s_vlan);
+
+    r = nm_platform_link_vlan_add(nm_device_get_platform(device),
+                                  iface,
+                                  parent_ifindex,
+                                  vlan_id,
+                                  nm_setting_vlan_get_flags(s_vlan),
+                                  out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create VLAN interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    nm_device_parent_set_ifindex(device, parent_ifindex);
+    if (vlan_id != priv->vlan_id) {
+        priv->vlan_id = vlan_id;
+        _notify((NMDeviceVlan *) device, PROP_VLAN_ID);
+    }
+
+    return TRUE;
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NMDeviceVlan *       self = NM_DEVICE_VLAN(device);
+    NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(self);
+
+    NM_DEVICE_CLASS(nm_device_vlan_parent_class)->unrealize_notify(device);
+
+    if (priv->vlan_id != 0) {
+        priv->vlan_id = 0;
+        _notify(self, PROP_VLAN_ID);
+    }
+}
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    /* We assume VLAN interfaces always support carrier detect */
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    if (!nm_device_parent_get_device(device))
+        return FALSE;
+    return NM_DEVICE_CLASS(nm_device_vlan_parent_class)->is_available(device, flags);
+}
+
+/*****************************************************************************/
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(device);
+    NMSettingVlan *      s_vlan;
+    const char *         parent;
+
+    if (!NM_DEVICE_CLASS(nm_device_vlan_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_device_is_real(device)) {
+        s_vlan = nm_connection_get_setting_vlan(connection);
+
+        if (nm_setting_vlan_get_id(s_vlan) != priv->vlan_id) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vlan id setting mismatches");
+            return FALSE;
+        }
+
+        /* Check parent interface; could be an interface name or a UUID */
+        parent = nm_setting_vlan_get_parent(s_vlan);
+        if (parent) {
+            if (!nm_device_match_parent(device, parent)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "vlan parent setting differs");
+                return FALSE;
+            }
+        } else {
+            /* Parent could be a MAC address in an NMSettingWired */
+            if (!nm_device_match_parent_hwaddr(device, connection, TRUE)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "vlan parent mac setting differs");
+                return FALSE;
+            }
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    if (!nm_device_is_real(device))
+        return TRUE;
+
+    return NM_DEVICE_CLASS(nm_device_vlan_parent_class)
+        ->check_connection_available(device, connection, flags, specific_object, error);
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingVlan *s_vlan;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_VLAN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("VLAN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_vlan = nm_connection_get_setting_vlan(connection);
+    if (!s_vlan) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'vlan' setting is required.");
+        return FALSE;
+    }
+
+    /* If there's no VLAN interface, no parent, and no hardware address in the
+     * settings, then there's not enough information to complete the setting.
+     */
+    if (!nm_setting_vlan_get_parent(s_vlan)
+        && !nm_device_match_parent_hwaddr(device, connection, TRUE)) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INVALID_CONNECTION,
+            "The 'vlan' setting had no interface name, parent, or hardware address.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceVlanPrivate * priv    = NM_DEVICE_VLAN_GET_PRIVATE(device);
+    NMSettingVlan *       s_vlan  = nm_connection_get_setting_vlan(connection);
+    int                   ifindex = nm_device_get_ifindex(device);
+    const NMPlatformLink *plink;
+    const NMPObject *     polnk;
+    guint                 vlan_id;
+    guint                 vlan_flags;
+
+    if (!s_vlan) {
+        s_vlan = (NMSettingVlan *) nm_setting_vlan_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_vlan);
+    }
+
+    polnk = nm_platform_link_get_lnk(nm_device_get_platform(device),
+                                     ifindex,
+                                     NM_LINK_TYPE_VLAN,
+                                     &plink);
+
+    if (polnk)
+        vlan_id = polnk->lnk_vlan.id;
+    else
+        vlan_id = priv->vlan_id;
+    if (vlan_id != nm_setting_vlan_get_id(s_vlan))
+        g_object_set(s_vlan, NM_SETTING_VLAN_ID, vlan_id, NULL);
+
+    g_object_set(s_vlan,
+                 NM_SETTING_VLAN_PARENT,
+                 nm_device_parent_find_for_connection(device, nm_setting_vlan_get_parent(s_vlan)),
+                 NULL);
+
+    if (polnk)
+        vlan_flags = polnk->lnk_vlan.flags;
+    else
+        vlan_flags = NM_VLAN_FLAG_REORDER_HEADERS;
+    if (vlan_flags != nm_setting_vlan_get_flags(s_vlan))
+        g_object_set(s_vlan, NM_SETTING_VLAN_FLAGS, (NMVlanFlags) vlan_flags, NULL);
+
+    if (polnk) {
+        _nm_setting_vlan_set_priorities(s_vlan,
+                                        NM_VLAN_INGRESS_MAP,
+                                        polnk->_lnk_vlan.ingress_qos_map,
+                                        polnk->_lnk_vlan.n_ingress_qos_map);
+        _nm_setting_vlan_set_priorities(s_vlan,
+                                        NM_VLAN_EGRESS_MAP,
+                                        polnk->_lnk_vlan.egress_qos_map,
+                                        polnk->_lnk_vlan.n_egress_qos_map);
+    } else {
+        _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_INGRESS_MAP, NULL, 0);
+        _nm_setting_vlan_set_priorities(s_vlan, NM_VLAN_EGRESS_MAP, NULL, 0);
+    }
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDevice *     parent_device;
+    NMSettingVlan *s_vlan;
+
+    /* Change MAC address to parent's one if needed */
+    parent_device = nm_device_parent_get_device(device);
+    if (parent_device) {
+        parent_hwaddr_maybe_changed(parent_device, NULL, device);
+        parent_mtu_maybe_changed(parent_device, NULL, device);
+    }
+
+    s_vlan = nm_device_get_applied_setting(device, NM_TYPE_SETTING_VLAN);
+    if (s_vlan) {
+        gs_free NMVlanQosMapping *ingress_map   = NULL;
+        gs_free NMVlanQosMapping *egress_map    = NULL;
+        guint                     n_ingress_map = 0;
+        guint                     n_egress_map  = 0;
+
+        _nm_setting_vlan_get_priorities(s_vlan, NM_VLAN_INGRESS_MAP, &ingress_map, &n_ingress_map);
+        _nm_setting_vlan_get_priorities(s_vlan, NM_VLAN_EGRESS_MAP, &egress_map, &n_egress_map);
+
+        nm_platform_link_vlan_change(nm_device_get_platform(device),
+                                     nm_device_get_ifindex(device),
+                                     NM_VLAN_FLAGS_ALL,
+                                     nm_setting_vlan_get_flags(s_vlan),
+                                     TRUE,
+                                     ingress_map,
+                                     n_ingress_map,
+                                     TRUE,
+                                     egress_map,
+                                     n_egress_map);
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_VLAN_ID:
+        g_value_set_uint(value, priv->vlan_id);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_vlan_init(NMDeviceVlan *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_vlan = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_VLAN,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("VlanId",
+                                                             "u",
+                                                             NM_DEVICE_VLAN_ID), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_vlan_class_init(NMDeviceVlanClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vlan);
+
+    device_class->connection_type_supported        = NM_SETTING_VLAN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_VLAN_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VLAN);
+    device_class->mtu_parent_delta                 = 0; /* VLANs can have the same MTU of parent */
+
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->link_changed                           = link_changed;
+    device_class->unrealize_notify                       = unrealize_notify;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->act_stage1_prepare                     = act_stage1_prepare;
+    device_class->get_configured_mtu    = nm_device_get_configured_mtu_wired_parent;
+    device_class->is_available          = is_available;
+    device_class->parent_changed_notify = parent_changed_notify;
+
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->update_connection           = update_connection;
+
+    obj_properties[PROP_VLAN_ID] = g_param_spec_uint(NM_DEVICE_VLAN_ID,
+                                                     "",
+                                                     "",
+                                                     0,
+                                                     4095,
+                                                     0,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_VLAN_DEVICE_FACTORY (nm_vlan_device_factory_get_type())
+#define NM_VLAN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VLAN_DEVICE_FACTORY, NMVlanDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_VLAN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_DRIVER,
+                        "8021q",
+                        NM_DEVICE_TYPE_DESC,
+                        "VLAN",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_VLAN,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_VLAN,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingVlan * s_vlan;
+    NMSettingWired *s_wired;
+    const char *    parent = NULL;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VLAN_SETTING_NAME), NULL);
+
+    s_vlan = nm_connection_get_setting_vlan(connection);
+    g_assert(s_vlan);
+
+    parent = nm_setting_vlan_get_parent(s_vlan);
+    if (parent)
+        return parent;
+
+    /* Try the hardware address from the VLAN connection's hardware setting */
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (s_wired)
+        return nm_setting_wired_get_mac_address(s_wired);
+
+    return NULL;
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    const char *   ifname;
+    NMSettingVlan *s_vlan;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VLAN_SETTING_NAME), NULL);
+
+    s_vlan = nm_connection_get_setting_vlan(connection);
+    g_assert(s_vlan);
+
+    if (!parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+    if (ifname)
+        return g_strdup(ifname);
+
+    /* If the connection doesn't specify the interface name for the VLAN
+     * device, we create one for it using the VLAN ID and the parent
+     * interface's name.
+     */
+    return nm_utils_new_vlan_name(parent_iface, nm_setting_vlan_get_id(s_vlan));
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    VLAN,
+    Vlan,
+    vlan,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VLAN)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VLAN_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-vlan.h b/src/core/devices/nm-device-vlan.h
new file mode 100644
index 00000000..5270706f
--- /dev/null
+++ b/src/core/devices/nm-device-vlan.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_VLAN_H__
+#define __NETWORKMANAGER_DEVICE_VLAN_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_VLAN (nm_device_vlan_get_type())
+#define NM_DEVICE_VLAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlan))
+#define NM_DEVICE_VLAN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VLAN, NMDeviceVlanClass))
+#define NM_IS_DEVICE_VLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VLAN))
+#define NM_IS_DEVICE_VLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VLAN))
+#define NM_DEVICE_VLAN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VLAN, NMDeviceVlanClass))
+
+typedef enum {
+    NM_VLAN_ERROR_CONNECTION_NOT_VLAN = 0, /*< nick=ConnectionNotVlan >*/
+    NM_VLAN_ERROR_CONNECTION_INVALID,      /*< nick=ConnectionInvalid >*/
+    NM_VLAN_ERROR_CONNECTION_INCOMPATIBLE, /*< nick=ConnectionIncompatible >*/
+} NMVlanError;
+
+#define NM_DEVICE_VLAN_ID "vlan-id"
+
+typedef struct _NMDeviceVlan      NMDeviceVlan;
+typedef struct _NMDeviceVlanClass NMDeviceVlanClass;
+
+GType nm_device_vlan_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_VLAN_H__ */
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
new file mode 100644
index 00000000..4fec59ba
--- /dev/null
+++ b/src/core/devices/nm-device-vrf.c
@@ -0,0 +1,375 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-vrf.h"
+
+#include "nm-core-internal.h"
+#include "nm-device-factory.h"
+#include "nm-device-private.h"
+#include "nm-manager.h"
+#include "nm-setting-vrf.h"
+#include "platform/nm-platform.h"
+#include "settings/nm-settings.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceVrf
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVrf, PROP_TABLE, );
+
+typedef struct {
+    NMPlatformLnkVrf props;
+} NMDeviceVrfPrivate;
+
+struct _NMDeviceVrf {
+    NMDevice           parent;
+    NMDeviceVrfPrivate _priv;
+};
+
+struct _NMDeviceVrfClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceVrf, nm_device_vrf, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_VRF_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceVrf, NM_IS_DEVICE_VRF, NMDevice)
+
+/*****************************************************************************/
+
+static void
+do_update_properties(NMDeviceVrf *self, const NMPlatformLnkVrf *props)
+{
+    NMDeviceVrfPrivate *priv   = NM_DEVICE_VRF_GET_PRIVATE(self);
+    GObject *           object = G_OBJECT(self);
+    NMPlatformLnkVrf    props_null;
+
+    if (!props) {
+        props_null = (NMPlatformLnkVrf){};
+        props      = &props_null;
+    }
+
+    g_object_freeze_notify(object);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(table, PROP_TABLE);
+
+    g_object_thaw_notify(object);
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceVrf *           self = NM_DEVICE_VRF(device);
+    const NMPlatformLnkVrf *props;
+
+    props = nm_platform_link_get_lnk_vrf(nm_device_get_platform(device),
+                                         nm_device_get_ifindex(device),
+                                         NULL);
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get vrf properties");
+        return;
+    }
+
+    do_update_properties(self, props);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_vrf_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NMDeviceVrf *self = NM_DEVICE_VRF(device);
+
+    NM_DEVICE_CLASS(nm_device_vrf_parent_class)->unrealize_notify(device);
+
+    do_update_properties(self, NULL);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *     iface = nm_device_get_iface(device);
+    NMPlatformLnkVrf props = {};
+    NMSettingVrf *   s_vrf;
+    int              r;
+
+    s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF);
+    nm_assert(s_vrf);
+
+    props.table = nm_setting_vrf_get_table(s_vrf);
+
+    r = nm_platform_link_vrf_add(nm_device_get_platform(device), iface, &props, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create VRF interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(device);
+    NMSettingVrf *      s_vrf;
+
+    if (!NM_DEVICE_CLASS(nm_device_vrf_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_device_is_real(device)) {
+        s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF);
+
+        if (priv->props.table != nm_setting_vrf_get_table(s_vrf)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vrf table mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingVrf *s_vrf;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_VRF_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("VRF connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF);
+    if (!s_vrf) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'vrf' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceVrfPrivate *priv  = NM_DEVICE_VRF_GET_PRIVATE(device);
+    NMSettingVrf *      s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF);
+
+    if (!s_vrf) {
+        s_vrf = (NMSettingVrf *) nm_setting_vrf_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_vrf);
+    }
+
+    if (priv->props.table != nm_setting_vrf_get_table(s_vrf))
+        g_object_set(G_OBJECT(s_vrf), NM_SETTING_VRF_TABLE, priv->props.table, NULL);
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    NMDeviceVrf *self        = NM_DEVICE_VRF(device);
+    gboolean     success     = TRUE;
+    const char * slave_iface = nm_device_get_ip_iface(slave);
+
+    nm_device_master_check_slave_physical_port(device, slave, LOGD_DEVICE);
+
+    if (configure) {
+        nm_device_take_down(slave, TRUE);
+        success = nm_platform_link_enslave(nm_device_get_platform(device),
+                                           nm_device_get_ip_ifindex(device),
+                                           nm_device_get_ip_ifindex(slave));
+        nm_device_bring_up(slave, TRUE, NULL);
+
+        if (!success)
+            return FALSE;
+
+        _LOGI(LOGD_DEVICE, "enslaved VRF slave %s", slave_iface);
+    } else
+        _LOGI(LOGD_BOND, "VRF slave %s was enslaved", slave_iface);
+
+    return TRUE;
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{
+    NMDeviceVrf *self = NM_DEVICE_VRF(device);
+    gboolean     success;
+    int          ifindex_slave;
+    int          ifindex;
+
+    if (configure) {
+        ifindex = nm_device_get_ifindex(device);
+        if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex))
+            configure = FALSE;
+    }
+
+    ifindex_slave = nm_device_get_ip_ifindex(slave);
+
+    if (ifindex_slave <= 0)
+        _LOGD(LOGD_DEVICE, "VRF slave %s is already released", nm_device_get_ip_iface(slave));
+
+    if (configure) {
+        if (ifindex_slave > 0) {
+            success = nm_platform_link_release(nm_device_get_platform(device),
+                                               nm_device_get_ip_ifindex(device),
+                                               ifindex_slave);
+
+            if (success) {
+                _LOGI(LOGD_DEVICE, "released VRF slave %s", nm_device_get_ip_iface(slave));
+            } else {
+                _LOGW(LOGD_DEVICE, "failed to release VRF slave %s", nm_device_get_ip_iface(slave));
+            }
+        }
+    } else {
+        if (ifindex_slave > 0) {
+            _LOGI(LOGD_DEVICE, "VRF slave %s was released", nm_device_get_ip_iface(slave));
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_TABLE:
+        g_value_set_uint(value, priv->props.table);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_vrf_init(NMDeviceVrf *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_vrf = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_VRF,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Table", "u", NM_DEVICE_VRF_TABLE), ), ),
+};
+
+static void
+nm_device_vrf_class_init(NMDeviceVrfClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vrf);
+
+    device_class->connection_type_supported        = NM_SETTING_VRF_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_VRF_SETTING_NAME;
+    device_class->is_master                        = TRUE;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VRF);
+
+    device_class->enslave_slave               = enslave_slave;
+    device_class->release_slave               = release_slave;
+    device_class->link_changed                = link_changed;
+    device_class->unrealize_notify            = unrealize_notify;
+    device_class->create_and_realize          = create_and_realize;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+    device_class->get_generic_capabilities    = get_generic_capabilities;
+    device_class->update_connection           = update_connection;
+
+    obj_properties[PROP_TABLE] = g_param_spec_uint(NM_DEVICE_VRF_TABLE,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT32,
+                                                   0,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_VRF_DEVICE_FACTORY (nm_vrf_device_factory_get_type())
+#define NM_VRF_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VRF_DEVICE_FACTORY, NMVrfDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_VRF,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Vrf",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_VRF,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_VRF,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    VRF,
+    Vrf,
+    vrf,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VRF)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VRF_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-vrf.h b/src/core/devices/nm-device-vrf.h
new file mode 100644
index 00000000..5169041c
--- /dev/null
+++ b/src/core/devices/nm-device-vrf.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef __NETWORKMANAGER_DEVICE_VRF_H__
+#define __NETWORKMANAGER_DEVICE_VRF_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_VRF (nm_device_vrf_get_type())
+#define NM_DEVICE_VRF(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrf))
+#define NM_DEVICE_VRF_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VRF, NMDeviceVrfClass))
+#define NM_IS_DEVICE_VRF(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VRF))
+#define NM_IS_DEVICE_VRF_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VRF))
+#define NM_DEVICE_VRF_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VRF, NMDeviceVrfClass))
+
+#define NM_DEVICE_VRF_TABLE "table"
+
+typedef struct _NMDeviceVrf      NMDeviceVrf;
+typedef struct _NMDeviceVrfClass NMDeviceVrfClass;
+
+GType nm_device_vrf_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_VRF_H__ */
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
new file mode 100644
index 00000000..f16a52c4
--- /dev/null
+++ b/src/core/devices/nm-device-vxlan.c
@@ -0,0 +1,813 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-vxlan.h"
+
+#include "nm-device-private.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+#include "nm-utils.h"
+#include "nm-device-factory.h"
+#include "nm-setting-vxlan.h"
+#include "nm-setting-wired.h"
+#include "settings/nm-settings.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceVxlan
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceVxlan,
+                             PROP_ID,
+                             PROP_LOCAL,
+                             PROP_GROUP,
+                             PROP_TOS,
+                             PROP_TTL,
+                             PROP_LEARNING,
+                             PROP_AGEING,
+                             PROP_LIMIT,
+                             PROP_SRC_PORT_MIN,
+                             PROP_SRC_PORT_MAX,
+                             PROP_DST_PORT,
+                             PROP_PROXY,
+                             PROP_RSC,
+                             PROP_L2MISS,
+                             PROP_L3MISS, );
+
+typedef struct {
+    NMPlatformLnkVxlan props;
+} NMDeviceVxlanPrivate;
+
+struct _NMDeviceVxlan {
+    NMDevice             parent;
+    NMDeviceVxlanPrivate _priv;
+};
+
+struct _NMDeviceVxlanClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceVxlan, nm_device_vxlan, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_VXLAN_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceVxlan, NM_IS_DEVICE_VXLAN, NMDevice)
+
+/*****************************************************************************/
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceVxlan *           self   = NM_DEVICE_VXLAN(device);
+    NMDeviceVxlanPrivate *    priv   = NM_DEVICE_VXLAN_GET_PRIVATE(self);
+    GObject *                 object = G_OBJECT(device);
+    const NMPlatformLnkVxlan *props;
+
+    props = nm_platform_link_get_lnk_vxlan(nm_device_get_platform(device),
+                                           nm_device_get_ifindex(device),
+                                           NULL);
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get vxlan properties");
+        return;
+    }
+
+    g_object_freeze_notify(object);
+
+    if (priv->props.parent_ifindex != props->parent_ifindex)
+        nm_device_parent_set_ifindex(device, props->parent_ifindex);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+#define CHECK_PROPERTY_CHANGED_IN6ADDR(field, prop)                                 \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        if (memcmp(&priv->props.field, &props->field, sizeof(props->field)) != 0) { \
+            priv->props.field = props->field;                                       \
+            _notify(self, prop);                                                    \
+        }                                                                           \
+    }                                                                               \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(id, PROP_ID);
+    CHECK_PROPERTY_CHANGED(local, PROP_LOCAL);
+    CHECK_PROPERTY_CHANGED_IN6ADDR(local6, PROP_LOCAL);
+    CHECK_PROPERTY_CHANGED(group, PROP_GROUP);
+    CHECK_PROPERTY_CHANGED_IN6ADDR(group6, PROP_GROUP);
+    CHECK_PROPERTY_CHANGED(tos, PROP_TOS);
+    CHECK_PROPERTY_CHANGED(ttl, PROP_TTL);
+    CHECK_PROPERTY_CHANGED(learning, PROP_LEARNING);
+    CHECK_PROPERTY_CHANGED(ageing, PROP_AGEING);
+    CHECK_PROPERTY_CHANGED(limit, PROP_LIMIT);
+    CHECK_PROPERTY_CHANGED(src_port_min, PROP_SRC_PORT_MIN);
+    CHECK_PROPERTY_CHANGED(src_port_max, PROP_SRC_PORT_MAX);
+    CHECK_PROPERTY_CHANGED(dst_port, PROP_DST_PORT);
+    CHECK_PROPERTY_CHANGED(proxy, PROP_PROXY);
+    CHECK_PROPERTY_CHANGED(rsc, PROP_RSC);
+    CHECK_PROPERTY_CHANGED(l2miss, PROP_L2MISS);
+    CHECK_PROPERTY_CHANGED(l3miss, PROP_L3MISS);
+
+    g_object_thaw_notify(object);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_vxlan_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static void
+unrealize_notify(NMDevice *device)
+{
+    NMDeviceVxlan *       self = NM_DEVICE_VXLAN(device);
+    NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(self);
+    guint                 i;
+
+    NM_DEVICE_CLASS(nm_device_vxlan_parent_class)->unrealize_notify(device);
+
+    memset(&priv->props, 0, sizeof(NMPlatformLnkVxlan));
+
+    for (i = 1; i < _PROPERTY_ENUMS_LAST; i++)
+        g_object_notify_by_pspec(G_OBJECT(self), obj_properties[i]);
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *       iface = nm_device_get_iface(device);
+    NMPlatformLnkVxlan props = {};
+    NMSettingVxlan *   s_vxlan;
+    const char *       str;
+    int                r;
+
+    s_vxlan = nm_connection_get_setting_vxlan(connection);
+    g_return_val_if_fail(s_vxlan, FALSE);
+
+    if (parent)
+        props.parent_ifindex = nm_device_get_ifindex(parent);
+
+    props.id = nm_setting_vxlan_get_id(s_vxlan);
+
+    str = nm_setting_vxlan_get_local(s_vxlan);
+    if (str) {
+        if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.local)
+            && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.local6))
+            return FALSE;
+    }
+
+    str = nm_setting_vxlan_get_remote(s_vxlan);
+    if (str) {
+        if (!nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &props.group)
+            && !nm_utils_parse_inaddr_bin(AF_INET6, str, NULL, &props.group6))
+            return FALSE;
+    }
+
+    props.tos          = nm_setting_vxlan_get_tos(s_vxlan);
+    props.ttl          = nm_setting_vxlan_get_ttl(s_vxlan);
+    props.learning     = nm_setting_vxlan_get_learning(s_vxlan);
+    props.ageing       = nm_setting_vxlan_get_ageing(s_vxlan);
+    props.limit        = nm_setting_vxlan_get_limit(s_vxlan);
+    props.src_port_min = nm_setting_vxlan_get_source_port_min(s_vxlan);
+    props.src_port_max = nm_setting_vxlan_get_source_port_max(s_vxlan);
+    props.dst_port     = nm_setting_vxlan_get_destination_port(s_vxlan);
+    props.proxy        = nm_setting_vxlan_get_proxy(s_vxlan);
+    props.rsc          = nm_setting_vxlan_get_rsc(s_vxlan);
+    props.l2miss       = nm_setting_vxlan_get_l2_miss(s_vxlan);
+    props.l3miss       = nm_setting_vxlan_get_l3_miss(s_vxlan);
+
+    r = nm_platform_link_vxlan_add(nm_device_get_platform(device), iface, &props, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create VXLAN interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6)
+{
+    NMIPAddr candidate_addr;
+    int      addr_family;
+
+    if (!candidate)
+        return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6);
+
+    if (!nm_utils_parse_inaddr_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
+        return FALSE;
+
+    if (!nm_ip_addr_equal(addr_family,
+                          &candidate_addr,
+                          NM_IS_IPv4(addr_family) ? (gpointer) &addr4 : addr6))
+        return FALSE;
+
+    if (NM_IS_IPv4(addr_family))
+        return IN6_IS_ADDR_UNSPECIFIED(addr6);
+    else
+        return addr4 == 0u;
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device);
+    NMSettingVxlan *      s_vxlan;
+    const char *          parent;
+
+    if (!NM_DEVICE_CLASS(nm_device_vxlan_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (nm_device_is_real(device)) {
+        s_vxlan = nm_connection_get_setting_vxlan(connection);
+
+        parent = nm_setting_vxlan_get_parent(s_vxlan);
+        if (parent && !nm_device_match_parent(device, parent)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan parent mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.id != nm_setting_vxlan_get_id(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan id mismatches");
+            return FALSE;
+        }
+
+        if (!address_matches(nm_setting_vxlan_get_local(s_vxlan),
+                             priv->props.local,
+                             &priv->props.local6)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan local address mismatches");
+            return FALSE;
+        }
+
+        if (!address_matches(nm_setting_vxlan_get_remote(s_vxlan),
+                             priv->props.group,
+                             &priv->props.group6)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan remote address mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.src_port_min != nm_setting_vxlan_get_source_port_min(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan source port min mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.src_port_max != nm_setting_vxlan_get_source_port_max(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan source port max mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.dst_port != nm_setting_vxlan_get_destination_port(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan destination port mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.tos != nm_setting_vxlan_get_tos(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan TOS mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.ttl != nm_setting_vxlan_get_ttl(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan TTL mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.learning != nm_setting_vxlan_get_learning(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan learning mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.ageing != nm_setting_vxlan_get_ageing(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan ageing mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.proxy != nm_setting_vxlan_get_proxy(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan proxy mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.rsc != nm_setting_vxlan_get_rsc(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan rsc mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.l2miss != nm_setting_vxlan_get_l2_miss(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan l2miss mismatches");
+            return FALSE;
+        }
+
+        if (priv->props.l3miss != nm_setting_vxlan_get_l3_miss(s_vxlan)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "vxlan l3miss mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingVxlan *s_vxlan;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_VXLAN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("VXLAN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_vxlan = nm_connection_get_setting_vxlan(connection);
+    if (!s_vxlan) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'vxlan' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceVxlanPrivate *priv    = NM_DEVICE_VXLAN_GET_PRIVATE(device);
+    NMSettingVxlan *      s_vxlan = nm_connection_get_setting_vxlan(connection);
+    char                  sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+    if (!s_vxlan) {
+        s_vxlan = (NMSettingVxlan *) nm_setting_vxlan_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_vxlan);
+    }
+
+    if (priv->props.id != nm_setting_vxlan_get_id(s_vxlan))
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_ID, priv->props.id, NULL);
+
+    g_object_set(s_vxlan,
+                 NM_SETTING_VXLAN_PARENT,
+                 nm_device_parent_find_for_connection(device, nm_setting_vxlan_get_parent(s_vxlan)),
+                 NULL);
+
+    if (!address_matches(nm_setting_vxlan_get_remote(s_vxlan),
+                         priv->props.group,
+                         &priv->props.group6)) {
+        if (priv->props.group) {
+            g_object_set(s_vxlan,
+                         NM_SETTING_VXLAN_REMOTE,
+                         _nm_utils_inet4_ntop(priv->props.group, sbuf),
+                         NULL);
+        } else {
+            g_object_set(s_vxlan,
+                         NM_SETTING_VXLAN_REMOTE,
+                         _nm_utils_inet6_ntop(&priv->props.group6, sbuf),
+                         NULL);
+        }
+    }
+
+    if (!address_matches(nm_setting_vxlan_get_local(s_vxlan),
+                         priv->props.local,
+                         &priv->props.local6)) {
+        if (priv->props.local) {
+            g_object_set(s_vxlan,
+                         NM_SETTING_VXLAN_LOCAL,
+                         _nm_utils_inet4_ntop(priv->props.local, sbuf),
+                         NULL);
+        } else if (memcmp(&priv->props.local6, &in6addr_any, sizeof(in6addr_any))) {
+            g_object_set(s_vxlan,
+                         NM_SETTING_VXLAN_LOCAL,
+                         _nm_utils_inet6_ntop(&priv->props.local6, sbuf),
+                         NULL);
+        }
+    }
+
+    if (priv->props.src_port_min != nm_setting_vxlan_get_source_port_min(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan),
+                     NM_SETTING_VXLAN_SOURCE_PORT_MIN,
+                     priv->props.src_port_min,
+                     NULL);
+    }
+
+    if (priv->props.src_port_max != nm_setting_vxlan_get_source_port_max(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan),
+                     NM_SETTING_VXLAN_SOURCE_PORT_MAX,
+                     priv->props.src_port_max,
+                     NULL);
+    }
+
+    if (priv->props.dst_port != nm_setting_vxlan_get_destination_port(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan),
+                     NM_SETTING_VXLAN_DESTINATION_PORT,
+                     priv->props.dst_port,
+                     NULL);
+    }
+
+    if (priv->props.tos != nm_setting_vxlan_get_tos(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_TOS, priv->props.tos, NULL);
+    }
+
+    if (priv->props.ttl != nm_setting_vxlan_get_ttl(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_TTL, priv->props.ttl, NULL);
+    }
+
+    if (priv->props.learning != nm_setting_vxlan_get_learning(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_LEARNING, priv->props.learning, NULL);
+    }
+
+    if (priv->props.ageing != nm_setting_vxlan_get_ageing(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_AGEING, priv->props.ageing, NULL);
+    }
+
+    if (priv->props.proxy != nm_setting_vxlan_get_proxy(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_PROXY, priv->props.proxy, NULL);
+    }
+
+    if (priv->props.rsc != nm_setting_vxlan_get_rsc(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_RSC, priv->props.rsc, NULL);
+    }
+
+    if (priv->props.l2miss != nm_setting_vxlan_get_l2_miss(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_L2_MISS, priv->props.l2miss, NULL);
+    }
+
+    if (priv->props.l3miss != nm_setting_vxlan_get_l3_miss(s_vxlan)) {
+        g_object_set(G_OBJECT(s_vxlan), NM_SETTING_VXLAN_L3_MISS, priv->props.l3miss, NULL);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_ID:
+        g_value_set_uint(value, priv->props.id);
+        break;
+    case PROP_GROUP:
+        if (priv->props.group)
+            g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.group));
+        else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.group6))
+            g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.group6));
+        break;
+    case PROP_LOCAL:
+        if (priv->props.local)
+            g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.local));
+        else if (!IN6_IS_ADDR_UNSPECIFIED(&priv->props.local6))
+            g_value_take_string(value, nm_utils_inet6_ntop_dup(&priv->props.local6));
+        break;
+    case PROP_TOS:
+        g_value_set_uchar(value, priv->props.tos);
+        break;
+    case PROP_TTL:
+        g_value_set_uchar(value, priv->props.ttl);
+        break;
+    case PROP_LEARNING:
+        g_value_set_boolean(value, priv->props.learning);
+        break;
+    case PROP_AGEING:
+        g_value_set_uint(value, priv->props.ageing);
+        break;
+    case PROP_LIMIT:
+        g_value_set_uint(value, priv->props.limit);
+        break;
+    case PROP_DST_PORT:
+        g_value_set_uint(value, priv->props.dst_port);
+        break;
+    case PROP_SRC_PORT_MIN:
+        g_value_set_uint(value, priv->props.src_port_min);
+        break;
+    case PROP_SRC_PORT_MAX:
+        g_value_set_uint(value, priv->props.src_port_max);
+        break;
+    case PROP_PROXY:
+        g_value_set_boolean(value, priv->props.proxy);
+        break;
+    case PROP_RSC:
+        g_value_set_boolean(value, priv->props.rsc);
+        break;
+    case PROP_L2MISS:
+        g_value_set_boolean(value, priv->props.l2miss);
+        break;
+    case PROP_L3MISS:
+        g_value_set_boolean(value, priv->props.l3miss);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_vxlan_init(NMDeviceVxlan *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_vxlan = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_VXLAN,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Parent", "o", NM_DEVICE_PARENT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Id", "u", NM_DEVICE_VXLAN_ID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Group", "s", NM_DEVICE_VXLAN_GROUP),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Local", "s", NM_DEVICE_VXLAN_LOCAL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Tos", "y", NM_DEVICE_VXLAN_TOS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ttl", "y", NM_DEVICE_VXLAN_TTL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Learning",
+                                                             "b",
+                                                             NM_DEVICE_VXLAN_LEARNING),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ageing", "u", NM_DEVICE_VXLAN_AGEING),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Limit", "u", NM_DEVICE_VXLAN_LIMIT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DstPort",
+                                                             "q",
+                                                             NM_DEVICE_VXLAN_DST_PORT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("SrcPortMin",
+                                                             "q",
+                                                             NM_DEVICE_VXLAN_SRC_PORT_MIN),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("SrcPortMax",
+                                                             "q",
+                                                             NM_DEVICE_VXLAN_SRC_PORT_MAX),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Proxy", "b", NM_DEVICE_VXLAN_PROXY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Rsc", "b", NM_DEVICE_VXLAN_RSC),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("L2miss", "b", NM_DEVICE_VXLAN_L2MISS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("L3miss",
+                                                             "b",
+                                                             NM_DEVICE_VXLAN_L3MISS), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_vxlan_class_init(NMDeviceVxlanClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_vxlan);
+
+    device_class->connection_type_supported        = NM_SETTING_VXLAN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_VXLAN_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_VXLAN);
+
+    device_class->link_changed                           = link_changed;
+    device_class->unrealize_notify                       = unrealize_notify;
+    device_class->create_and_realize                     = create_and_realize;
+    device_class->check_connection_compatible            = check_connection_compatible;
+    device_class->complete_connection                    = complete_connection;
+    device_class->get_generic_capabilities               = get_generic_capabilities;
+    device_class->update_connection                      = update_connection;
+    device_class->act_stage1_prepare_set_hwaddr_ethernet = TRUE;
+    device_class->get_configured_mtu                     = nm_device_get_configured_mtu_for_wired;
+
+    obj_properties[PROP_ID] = g_param_spec_uint(NM_DEVICE_VXLAN_ID,
+                                                "",
+                                                "",
+                                                0,
+                                                G_MAXUINT32,
+                                                0,
+                                                G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LOCAL] = g_param_spec_string(NM_DEVICE_VXLAN_LOCAL,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_GROUP] = g_param_spec_string(NM_DEVICE_VXLAN_GROUP,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TOS] = g_param_spec_uchar(NM_DEVICE_VXLAN_TOS,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_TTL] = g_param_spec_uchar(NM_DEVICE_VXLAN_TTL,
+                                                  "",
+                                                  "",
+                                                  0,
+                                                  255,
+                                                  0,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LEARNING] = g_param_spec_boolean(NM_DEVICE_VXLAN_LEARNING,
+                                                         "",
+                                                         "",
+                                                         FALSE,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_AGEING] = g_param_spec_uint(NM_DEVICE_VXLAN_AGEING,
+                                                    "",
+                                                    "",
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LIMIT] = g_param_spec_uint(NM_DEVICE_VXLAN_LIMIT,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT32,
+                                                   0,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SRC_PORT_MIN] =
+        g_param_spec_uint(NM_DEVICE_VXLAN_SRC_PORT_MIN,
+                          "",
+                          "",
+                          0,
+                          65535,
+                          0,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SRC_PORT_MAX] =
+        g_param_spec_uint(NM_DEVICE_VXLAN_SRC_PORT_MAX,
+                          "",
+                          "",
+                          0,
+                          65535,
+                          0,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DST_PORT] = g_param_spec_uint(NM_DEVICE_VXLAN_DST_PORT,
+                                                      "",
+                                                      "",
+                                                      0,
+                                                      65535,
+                                                      0,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PROXY] = g_param_spec_boolean(NM_DEVICE_VXLAN_PROXY,
+                                                      "",
+                                                      "",
+                                                      FALSE,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_RSC] = g_param_spec_boolean(NM_DEVICE_VXLAN_RSC,
+                                                    "",
+                                                    "",
+                                                    FALSE,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_L2MISS] = g_param_spec_boolean(NM_DEVICE_VXLAN_L2MISS,
+                                                       "",
+                                                       "",
+                                                       FALSE,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_L3MISS] = g_param_spec_boolean(NM_DEVICE_VXLAN_L3MISS,
+                                                       "",
+                                                       "",
+                                                       FALSE,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_VXLAN_DEVICE_FACTORY (nm_vxlan_device_factory_get_type())
+#define NM_VXLAN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VXLAN_DEVICE_FACTORY, NMVxlanDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_VXLAN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Vxlan",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_VXLAN,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_VXLAN,
+                        NULL);
+}
+
+static const char *
+get_connection_parent(NMDeviceFactory *factory, NMConnection *connection)
+{
+    NMSettingVxlan *s_vxlan;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VXLAN_SETTING_NAME), NULL);
+
+    s_vxlan = nm_connection_get_setting_vxlan(connection);
+    g_assert(s_vxlan);
+
+    return nm_setting_vxlan_get_parent(s_vxlan);
+}
+
+static char *
+get_connection_iface(NMDeviceFactory *factory, NMConnection *connection, const char *parent_iface)
+{
+    const char *    ifname;
+    NMSettingVxlan *s_vxlan;
+
+    g_return_val_if_fail(nm_connection_is_type(connection, NM_SETTING_VXLAN_SETTING_NAME), NULL);
+
+    s_vxlan = nm_connection_get_setting_vxlan(connection);
+    g_assert(s_vxlan);
+
+    if (nm_setting_vxlan_get_parent(s_vxlan) && !parent_iface)
+        return NULL;
+
+    ifname = nm_connection_get_interface_name(connection);
+    return g_strdup(ifname);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    VXLAN,
+    Vxlan,
+    vxlan,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_VXLAN)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_VXLAN_SETTING_NAME),
+    factory_class->create_device         = create_device;
+    factory_class->get_connection_parent = get_connection_parent;
+    factory_class->get_connection_iface  = get_connection_iface;);
diff --git a/src/core/devices/nm-device-vxlan.h b/src/core/devices/nm-device-vxlan.h
new file mode 100644
index 00000000..4c4165e5
--- /dev/null
+++ b/src/core/devices/nm-device-vxlan.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013, 2014 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_VXLAN_H__
+#define __NETWORKMANAGER_DEVICE_VXLAN_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_VXLAN (nm_device_vxlan_get_type())
+#define NM_DEVICE_VXLAN(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlan))
+#define NM_DEVICE_VXLAN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlanClass))
+#define NM_IS_DEVICE_VXLAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_VXLAN))
+#define NM_IS_DEVICE_VXLAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_VXLAN))
+#define NM_DEVICE_VXLAN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_VXLAN, NMDeviceVxlanClass))
+
+#define NM_DEVICE_VXLAN_ID           "id"
+#define NM_DEVICE_VXLAN_GROUP        "group"
+#define NM_DEVICE_VXLAN_LOCAL        "local"
+#define NM_DEVICE_VXLAN_TOS          "tos"
+#define NM_DEVICE_VXLAN_TTL          "ttl"
+#define NM_DEVICE_VXLAN_LEARNING     "learning"
+#define NM_DEVICE_VXLAN_AGEING       "ageing"
+#define NM_DEVICE_VXLAN_LIMIT        "limit"
+#define NM_DEVICE_VXLAN_DST_PORT     "dst-port"
+#define NM_DEVICE_VXLAN_SRC_PORT_MIN "src-port-min"
+#define NM_DEVICE_VXLAN_SRC_PORT_MAX "src-port-max"
+#define NM_DEVICE_VXLAN_PROXY        "proxy"
+#define NM_DEVICE_VXLAN_RSC          "rsc"
+#define NM_DEVICE_VXLAN_L2MISS       "l2miss"
+#define NM_DEVICE_VXLAN_L3MISS       "l3miss"
+
+typedef struct _NMDeviceVxlan      NMDeviceVxlan;
+typedef struct _NMDeviceVxlanClass NMDeviceVxlanClass;
+
+GType nm_device_vxlan_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_VXLAN_H__ */
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
new file mode 100644
index 00000000..fd057ded
--- /dev/null
+++ b/src/core/devices/nm-device-wireguard.c
@@ -0,0 +1,2093 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Javier Arteaga <jarteaga@jbeta.is>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-wireguard.h"
+
+#include <linux/rtnetlink.h>
+#include <linux/fib_rules.h>
+
+#include "nm-setting-wireguard.h"
+#include "nm-core-internal.h"
+#include "nm-glib-aux/nm-secret-utils.h"
+#include "nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "platform/nmp-object.h"
+#include "platform/nmp-rules-manager.h"
+#include "nm-device-factory.h"
+#include "nm-active-connection.h"
+#include "nm-act-request.h"
+#include "dns/nm-dns-manager.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceWireGuard
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+/* TODO: activate profile with peer preshared-key-flags=2. On first activation, the secret is
+ *   requested (good). Enter it and connect. Reactivate the profile, now there is no password
+ *   prompt, as the secret is cached (good??). */
+
+/* TODO: unlike for other VPNs, we don't inject a direct route to the peers. That means,
+ *   you might get a routing scenario where the peer (VPN server) is reachable via the VPN.
+ *   How we handle adding routes to external gateway for other peers, has severe issues
+ *   as well. We may use policy-routing like wg-quick does. See also discussions at
+ *   https://www.wireguard.com/netns/#improving-the-classic-solutions */
+
+/* TODO: honor the TTL of DNS to determine when to retry resolving endpoints. */
+
+/* TODO: when we get multiple IP addresses when resolving a peer endpoint. We currently
+ *   just take the first from GAI. We should only accept AAAA/IPv6 if we also have a suitable
+ *   IPv6 address. The problem is, that we have to recheck that when IP addressing on other
+ *   interfaces changes. This makes it almost too cumbersome to implement. */
+
+/*****************************************************************************/
+
+G_STATIC_ASSERT(NM_WIREGUARD_PUBLIC_KEY_LEN == NMP_WIREGUARD_PUBLIC_KEY_LEN);
+G_STATIC_ASSERT(NM_WIREGUARD_SYMMETRIC_KEY_LEN == NMP_WIREGUARD_SYMMETRIC_KEY_LEN);
+
+/*****************************************************************************/
+
+#define LINK_CONFIG_RATE_LIMIT_NSEC (50 * NM_UTILS_NSEC_PER_MSEC)
+
+/* a special @next_try_at_nsec timestamp indicating that we should try again as soon as possible. */
+#define NEXT_TRY_AT_NSEC_ASAP ((gint64) G_MAXINT64)
+
+/* a special @next_try_at_nsec timestamp that is
+ *  - positive (indicating resolve-checks are enabled)
+ *  - already in the past (we use the absolute timestamp of 1nsec for that). */
+#define NEXT_TRY_AT_NSEC_PAST ((gint64) 1)
+
+/* like %NEXT_TRY_AT_NSEC_ASAP, but used for indicating to retry ASAP for a @retry_in_msec value.
+ * That is a relative time duration, contrary to @next_try_at_nsec which is an absolute
+ * timestamp. */
+#define RETRY_IN_MSEC_ASAP ((gint64) G_MAXINT64)
+
+#define RETRY_IN_MSEC_MAX ((gint64)(30 * 60 * 1000))
+
+typedef enum {
+    LINK_CONFIG_MODE_FULL,
+    LINK_CONFIG_MODE_REAPPLY,
+    LINK_CONFIG_MODE_ASSUME,
+    LINK_CONFIG_MODE_ENDPOINTS,
+} LinkConfigMode;
+
+typedef struct {
+    GCancellable *cancellable;
+
+    NMSockAddrUnion sockaddr;
+
+    /* the timestamp (in nm_utils_get_monotonic_timestamp_nsec() scale) when we want
+     * to retry resolving the endpoint (again).
+     *
+     * It may be set to %NEXT_TRY_AT_NSEC_ASAP to indicate to re-resolve as soon as possible.
+     *
+     * A @sockaddr is either fixed or it has
+     *   - @cancellable set to indicate an ongoing request
+     *   - @next_try_at_nsec set to a positive value, indicating when
+     *     we ought to retry. */
+    gint64 next_try_at_nsec;
+
+    guint resolv_fail_count;
+} PeerEndpointResolveData;
+
+typedef struct {
+    NMWireGuardPeer *peer;
+
+    NMDeviceWireGuard *self;
+
+    CList lst_peers;
+
+    PeerEndpointResolveData ep_resolv;
+
+    /* dirty flag used during _peers_update_all(). */
+    bool dirty_update_all : 1;
+} PeerData;
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWireGuard, PROP_PUBLIC_KEY, PROP_LISTEN_PORT, PROP_FWMARK, );
+
+typedef struct {
+    NMDnsManager *dns_manager;
+
+    NMPlatformLnkWireGuard        lnk_curr;
+    NMActRequestGetSecretsCallId *secrets_call_id;
+
+    CList       lst_peers_head;
+    GHashTable *peers;
+
+    /* counts the numbers of peers that are currently resolving. */
+    guint peers_resolving_cnt;
+
+    gint64 resolve_next_try_at;
+    gint64 link_config_last_at;
+
+    guint resolve_next_try_id;
+    guint link_config_delayed_id;
+
+    guint32 auto_default_route_fwmark;
+
+    guint32 auto_default_route_priority;
+
+    bool auto_default_route_enabled_4 : 1;
+    bool auto_default_route_enabled_6 : 1;
+    bool auto_default_route_initialized : 1;
+    bool auto_default_route_refresh : 1;
+    bool auto_default_route_priority_initialized : 1;
+
+} NMDeviceWireGuardPrivate;
+
+struct _NMDeviceWireGuard {
+    NMDevice                 parent;
+    NMDeviceWireGuardPrivate _priv;
+};
+
+struct _NMDeviceWireGuardClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceWireGuard, nm_device_wireguard, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_WIREGUARD_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceWireGuard, NM_IS_DEVICE_WIREGUARD, NMDevice)
+
+/*****************************************************************************/
+
+static void _peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data);
+
+static void _peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec);
+
+static gboolean link_config_delayed_resolver_cb(gpointer user_data);
+
+static gboolean link_config_delayed_ratelimit_cb(gpointer user_data);
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(_link_config_mode_to_string,
+                                  LinkConfigMode,
+                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL),
+                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_FULL, "full"),
+                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_REAPPLY, "reapply"),
+                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ASSUME, "assume"),
+                                  NM_UTILS_LOOKUP_ITEM(LINK_CONFIG_MODE_ENDPOINTS, "endpoints"), );
+
+/*****************************************************************************/
+
+static void
+_auto_default_route_get_enabled(NMSettingWireGuard *s_wg,
+                                NMConnection *      connection,
+                                gboolean *          out_enabled_v4,
+                                gboolean *          out_enabled_v6)
+{
+    NMTernary enabled_v4;
+    NMTernary enabled_v6;
+
+    enabled_v4 = nm_setting_wireguard_get_ip4_auto_default_route(s_wg);
+    enabled_v6 = nm_setting_wireguard_get_ip6_auto_default_route(s_wg);
+
+    if (enabled_v4 == NM_TERNARY_DEFAULT) {
+        if (nm_setting_ip_config_get_never_default(
+                nm_connection_get_setting_ip_config(connection, AF_INET)))
+            enabled_v4 = FALSE;
+    }
+    if (enabled_v6 == NM_TERNARY_DEFAULT) {
+        if (nm_setting_ip_config_get_never_default(
+                nm_connection_get_setting_ip_config(connection, AF_INET6)))
+            enabled_v6 = FALSE;
+    }
+
+    if (enabled_v4 == NM_TERNARY_DEFAULT || enabled_v6 == NM_TERNARY_DEFAULT) {
+        guint i, n_peers;
+
+        n_peers = nm_setting_wireguard_get_peers_len(s_wg);
+        for (i = 0; i < n_peers; i++) {
+            NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i);
+            guint            n_aips;
+            guint            j;
+
+            n_aips = nm_wireguard_peer_get_allowed_ips_len(peer);
+            for (j = 0; j < n_aips; j++) {
+                const char *aip;
+                gboolean    valid;
+                int         prefix;
+                int         addr_family;
+
+                aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
+                if (!valid)
+                    continue;
+                if (!nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix))
+                    continue;
+                if (prefix != 0)
+                    continue;
+
+                if (addr_family == AF_INET) {
+                    if (enabled_v4 == NM_TERNARY_DEFAULT) {
+                        enabled_v4 = TRUE;
+                        if (enabled_v6 != NM_TERNARY_DEFAULT)
+                            goto done;
+                    }
+                } else {
+                    if (enabled_v6 == NM_TERNARY_DEFAULT) {
+                        enabled_v6 = TRUE;
+                        if (enabled_v4 != NM_TERNARY_DEFAULT)
+                            goto done;
+                    }
+                }
+            }
+        }
+done:;
+    }
+
+    *out_enabled_v4 = (enabled_v4 == TRUE);
+    *out_enabled_v6 = (enabled_v6 == TRUE);
+}
+
+#define AUTO_RANDOM_RANGE 500u
+
+static guint32
+_auto_default_route_get_auto_fwmark(const char *uuid)
+{
+    guint64 rnd_seed;
+
+    /* we use the generated number as fwmark but also as routing table for
+     * the default-route.
+     *
+     * We pick a number
+     *
+     * - based on the connection's UUID (as stable seed).
+     * - larger than 51820u (arbitrarily)
+     * - one out of AUTO_RANDOM_RANGE
+     */
+
+    rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0xb9,
+                                              0x39,
+                                              0x8e,
+                                              0xed,
+                                              0x15,
+                                              0xb3,
+                                              0xd1,
+                                              0xc4,
+                                              0x5f,
+                                              0x45,
+                                              0x00,
+                                              0x4f,
+                                              0xec,
+                                              0xc2,
+                                              0x2b,
+                                              0x7e),
+                              (const guint8 *) uuid,
+                              uuid ? strlen(uuid) + 1u : 0u);
+
+    return 51820u + (rnd_seed % AUTO_RANDOM_RANGE);
+}
+
+#define PRIO_WIDTH 2u
+
+static guint32
+_auto_default_route_get_auto_priority(const char *uuid)
+{
+    const guint32 RANGE_TOP = 32766u - 1000u;
+    guint64       rnd_seed;
+
+    /* we pick a priority for the routing rules as follows:
+     *
+     * - use the connection's UUID as stable seed for the "random" number.
+     * - have it smaller than RANGE_TOP (32766u - 1000u), where 32766u is the priority of the default
+     *   rules
+     * - we add 2 rules (PRIO_WIDTH). Hence only pick even priorities.
+     * - pick one out of AUTO_RANDOM_RANGE. */
+
+    rnd_seed = c_siphash_hash(NM_HASH_SEED_16(0x99,
+                                              0x22,
+                                              0x4d,
+                                              0x7c,
+                                              0x37,
+                                              0xda,
+                                              0x8e,
+                                              0x7b,
+                                              0x2f,
+                                              0x55,
+                                              0x16,
+                                              0x7b,
+                                              0x75,
+                                              0xda,
+                                              0x42,
+                                              0xdc),
+                              (const guint8 *) uuid,
+                              uuid ? strlen(uuid) + 1u : 0u);
+
+    return RANGE_TOP - (((rnd_seed % (PRIO_WIDTH * AUTO_RANDOM_RANGE)) / PRIO_WIDTH) * PRIO_WIDTH);
+}
+
+static void
+_auto_default_route_init(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    NMConnection *            connection;
+    gboolean                  enabled_v4 = FALSE;
+    gboolean                  enabled_v6 = FALSE;
+    gboolean                  refreshing_only;
+    guint32                   new_fwmark = 0;
+    guint32                   old_fwmark;
+    char                      sbuf1[100];
+
+    if (G_LIKELY(priv->auto_default_route_initialized && !priv->auto_default_route_refresh))
+        return;
+
+    refreshing_only = priv->auto_default_route_initialized && priv->auto_default_route_refresh;
+
+    old_fwmark = priv->auto_default_route_fwmark;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+    if (connection) {
+        NMSettingWireGuard *s_wg;
+
+        s_wg = _nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD);
+
+        new_fwmark = nm_setting_wireguard_get_fwmark(s_wg);
+
+        _auto_default_route_get_enabled(s_wg, connection, &enabled_v4, &enabled_v6);
+    }
+
+    if ((enabled_v4 || enabled_v6) && new_fwmark == 0u) {
+        if (refreshing_only)
+            new_fwmark = old_fwmark;
+        else
+            new_fwmark = _auto_default_route_get_auto_fwmark(nm_connection_get_uuid(connection));
+    }
+
+    priv->auto_default_route_refresh     = FALSE;
+    priv->auto_default_route_fwmark      = new_fwmark;
+    priv->auto_default_route_enabled_4   = enabled_v4;
+    priv->auto_default_route_enabled_6   = enabled_v6;
+    priv->auto_default_route_initialized = TRUE;
+
+    if (connection) {
+        _LOGT(LOGD_DEVICE,
+              "auto-default-route is %s for IPv4 and %s for IPv6%s",
+              priv->auto_default_route_enabled_4 ? "enabled" : "disabled",
+              priv->auto_default_route_enabled_6 ? "enabled" : "disabled",
+              priv->auto_default_route_enabled_4 || priv->auto_default_route_enabled_6
+                  ? nm_sprintf_buf(sbuf1, " (fwmark 0x%x)", priv->auto_default_route_fwmark)
+                  : "");
+    }
+}
+
+static GPtrArray *
+get_extra_rules(NMDevice *device)
+{
+    NMDeviceWireGuard *       self           = NM_DEVICE_WIREGUARD(device);
+    NMDeviceWireGuardPrivate *priv           = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gs_unref_ptrarray GPtrArray *extra_rules = NULL;
+    guint32                      priority    = 0;
+    int                          is_ipv4;
+    NMConnection *               connection;
+
+    _auto_default_route_init(self);
+
+    connection = nm_device_get_applied_connection(device);
+    if (!connection)
+        return NULL;
+
+    for (is_ipv4 = 0; is_ipv4 < 2; is_ipv4++) {
+        NMSettingIPConfig *s_ip;
+        int                addr_family = is_ipv4 ? AF_INET : AF_INET6;
+        guint32            table_main;
+        guint32            fwmark;
+
+        if (is_ipv4) {
+            if (!priv->auto_default_route_enabled_4)
+                continue;
+        } else {
+            if (!priv->auto_default_route_enabled_6)
+                continue;
+        }
+
+        if (!extra_rules) {
+            if (priv->auto_default_route_priority_initialized)
+                priority = priv->auto_default_route_priority;
+            else {
+                priority =
+                    _auto_default_route_get_auto_priority(nm_connection_get_uuid(connection));
+                priv->auto_default_route_priority             = priority;
+                priv->auto_default_route_priority_initialized = TRUE;
+            }
+            extra_rules = g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
+        }
+
+        s_ip       = nm_connection_get_setting_ip_config(connection, addr_family);
+        table_main = nm_setting_ip_config_get_route_table(s_ip);
+        if (table_main == 0)
+            table_main = RT_TABLE_MAIN;
+
+        fwmark = priv->auto_default_route_fwmark;
+
+        G_STATIC_ASSERT_EXPR(PRIO_WIDTH == 2);
+
+        g_ptr_array_add(extra_rules,
+                        nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE,
+                                       &((const NMPlatformRoutingRule){
+                                           .priority                   = priority,
+                                           .addr_family                = addr_family,
+                                           .action                     = FR_ACT_TO_TBL,
+                                           .table                      = table_main,
+                                           .suppress_prefixlen_inverse = ~((guint32) 0u),
+                                       })));
+
+        g_ptr_array_add(extra_rules,
+                        nmp_object_new(NMP_OBJECT_TYPE_ROUTING_RULE,
+                                       &((const NMPlatformRoutingRule){
+                                           .priority    = priority + 1u,
+                                           .addr_family = addr_family,
+                                           .action      = FR_ACT_TO_TBL,
+                                           .table       = fwmark,
+                                           .flags       = FIB_RULE_INVERT,
+                                           .fwmark      = fwmark,
+                                           .fwmask      = 0xFFFFFFFFu,
+                                       })));
+    }
+
+    return g_steal_pointer(&extra_rules);
+}
+
+static guint32
+coerce_route_table(NMDevice *device, int addr_family, guint32 route_table, gboolean is_user_config)
+{
+    NMDeviceWireGuard *       self = NM_DEVICE_WIREGUARD(device);
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gboolean                  auto_default_route_enabled;
+
+    if (route_table != 0u)
+        return route_table;
+
+    _auto_default_route_init(self);
+
+    auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4
+                                                          : priv->auto_default_route_enabled_6;
+
+    if (auto_default_route_enabled) {
+        /* we need to enable full-sync mode of all routing tables. */
+        _LOGT(LOGD_DEVICE,
+              "coerce ipv%c.route-table setting to \"main\" (table 254) as we enable "
+              "auto-default-route handling",
+              nm_utils_addr_family_to_char(addr_family));
+        return RT_TABLE_MAIN;
+    }
+
+    return 0;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_peer_data_equal(gconstpointer ptr_a, gconstpointer ptr_b)
+{
+    const PeerData *peer_data_a = ptr_a;
+    const PeerData *peer_data_b = ptr_b;
+
+    return nm_streq(nm_wireguard_peer_get_public_key(peer_data_a->peer),
+                    nm_wireguard_peer_get_public_key(peer_data_b->peer));
+}
+
+static guint
+_peer_data_hash(gconstpointer ptr)
+{
+    const PeerData *peer_data = ptr;
+
+    return nm_hash_str(nm_wireguard_peer_get_public_key(peer_data->peer));
+}
+
+static PeerData *
+_peers_find(NMDeviceWireGuardPrivate *priv, NMWireGuardPeer *peer)
+{
+    nm_assert(peer);
+
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(PeerData, peer) == 0);
+
+    return g_hash_table_lookup(priv->peers, &peer);
+}
+
+static guint
+_peers_resolving_cnt(NMDeviceWireGuardPrivate *priv)
+{
+    nm_assert(priv);
+#if NM_MORE_ASSERTS > 3
+    {
+        PeerData *peer_data;
+        guint     cnt = 0;
+
+        c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
+            if (peer_data->ep_resolv.cancellable)
+                cnt++;
+        }
+        nm_assert(cnt == priv->peers_resolving_cnt);
+    }
+#endif
+
+    return priv->peers_resolving_cnt;
+}
+
+static void
+_peers_resolving_cnt_decrement(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    nm_assert(priv);
+    nm_assert(priv->peers_resolving_cnt > 0);
+
+    priv->peers_resolving_cnt--;
+
+    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
+
+    if (priv->peers_resolving_cnt == 0) {
+        if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_CONFIG) {
+            _LOGT(LOGD_DEVICE,
+                  "activation delayed to resolve DNS names of peers: completed, proceed now");
+            nm_device_activate_schedule_stage2_device_config(NM_DEVICE(self), FALSE);
+        }
+    }
+}
+
+static void
+_peers_remove(NMDeviceWireGuard *self, PeerData *peer_data)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    nm_assert(peer_data);
+    nm_assert(g_hash_table_lookup(priv->peers, peer_data) == peer_data);
+
+    if (!g_hash_table_remove(priv->peers, peer_data))
+        nm_assert_not_reached();
+
+    c_list_unlink_stale(&peer_data->lst_peers);
+    nm_wireguard_peer_unref(peer_data->peer);
+    if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable))
+        _peers_resolving_cnt_decrement(self);
+    g_slice_free(PeerData, peer_data);
+
+    if (c_list_is_empty(&priv->lst_peers_head)) {
+        nm_clear_g_source(&priv->resolve_next_try_id);
+        nm_clear_g_source(&priv->link_config_delayed_id);
+    }
+
+    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
+}
+
+static PeerData *
+_peers_add(NMDeviceWireGuard *self, NMWireGuardPeer *peer)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    PeerData *                peer_data;
+
+    nm_assert(peer);
+    nm_assert(nm_wireguard_peer_is_sealed(peer));
+    nm_assert(!_peers_find(priv, peer));
+
+    peer_data  = g_slice_new(PeerData);
+    *peer_data = (PeerData){
+        .self = self,
+        .peer = nm_wireguard_peer_ref(peer),
+        .ep_resolv =
+            {
+                .sockaddr = NM_SOCK_ADDR_UNION_INIT_UNSPEC,
+            },
+    };
+
+    c_list_link_tail(&priv->lst_peers_head, &peer_data->lst_peers);
+    if (!nm_g_hash_table_add(priv->peers, peer_data))
+        nm_assert_not_reached();
+    return peer_data;
+}
+
+static gboolean
+_peers_resolve_retry_timeout(gpointer user_data)
+{
+    NMDeviceWireGuard *       self = user_data;
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    PeerData *                peer_data;
+    gint64                    now;
+    gint64                    next;
+
+    priv->resolve_next_try_id = 0;
+
+    _LOGT(LOGD_DEVICE, "wireguard-peers: rechecking peer endpoints...");
+
+    now  = nm_utils_get_monotonic_timestamp_nsec();
+    next = G_MAXINT64;
+    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
+        if (peer_data->ep_resolv.next_try_at_nsec <= 0)
+            continue;
+
+        if (peer_data->ep_resolv.cancellable) {
+            /* we are currently resolving a name. We don't need the global
+             * watchdog to guard this peer. No need to adjust @next for
+             * this one, when the currently ongoing resolving completes, we
+             * may reschedule. Skip. */
+            continue;
+        }
+
+        if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP
+            || now >= peer_data->ep_resolv.next_try_at_nsec) {
+            _peers_resolve_start(self, peer_data);
+            /* same here. Now we are resolving. We don't need the global
+             * watchdog. Skip w.r.t. finding @next. */
+            continue;
+        }
+
+        if (next > peer_data->ep_resolv.next_try_at_nsec)
+            next = peer_data->ep_resolv.next_try_at_nsec;
+    }
+    if (next < G_MAXINT64)
+        _peers_resolve_retry_reschedule(self, next);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_peers_resolve_retry_reschedule(NMDeviceWireGuard *self, gint64 new_next_try_at_nsec)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    guint32                   interval_ms;
+    gint64                    now;
+
+    nm_assert(new_next_try_at_nsec > 0);
+    nm_assert(new_next_try_at_nsec != NEXT_TRY_AT_NSEC_ASAP);
+
+    if (priv->resolve_next_try_id && priv->resolve_next_try_at <= new_next_try_at_nsec) {
+        /* we already have an earlier timeout scheduled (possibly for
+         * another peer that expires sooner). Don't reschedule now.
+         * Even if the scheduled timeout expires too early, we will
+         * compute the right next-timeout and reschedule then. */
+        return;
+    }
+
+    now = nm_utils_get_monotonic_timestamp_nsec();
+
+    /* schedule at most one day ahead. No problem if we expire earlier
+     * than expected. Also, rate-limit to 500 msec. */
+    interval_ms = NM_CLAMP((new_next_try_at_nsec - now) / NM_UTILS_NSEC_PER_MSEC,
+                           (gint64) 500,
+                           (gint64)(24 * 60 * 60 * 1000));
+
+    _LOGT(LOGD_DEVICE,
+          "wireguard-peers: schedule rechecking peer endpoints in %u msec",
+          interval_ms);
+
+    nm_clear_g_source(&priv->resolve_next_try_id);
+    priv->resolve_next_try_at = new_next_try_at_nsec;
+    priv->resolve_next_try_id = g_timeout_add(interval_ms, _peers_resolve_retry_timeout, self);
+}
+
+static void
+_peers_resolve_retry_reschedule_for_peer(NMDeviceWireGuard *self,
+                                         PeerData *         peer_data,
+                                         gint64             retry_in_msec)
+{
+    nm_assert(retry_in_msec >= 0);
+
+    if (retry_in_msec == RETRY_IN_MSEC_ASAP) {
+        _peers_resolve_start(self, peer_data);
+        return;
+    }
+
+    peer_data->ep_resolv.next_try_at_nsec =
+        nm_utils_get_monotonic_timestamp_nsec() + (retry_in_msec * NM_UTILS_NSEC_PER_MSEC);
+    _peers_resolve_retry_reschedule(self, peer_data->ep_resolv.next_try_at_nsec);
+}
+
+static gint64
+_peers_retry_in_msec(PeerData *peer_data, gboolean after_failure)
+{
+    if (peer_data->ep_resolv.next_try_at_nsec == NEXT_TRY_AT_NSEC_ASAP) {
+        peer_data->ep_resolv.resolv_fail_count = 0;
+        return RETRY_IN_MSEC_ASAP;
+    }
+
+    if (after_failure) {
+        if (peer_data->ep_resolv.resolv_fail_count < G_MAXUINT)
+            peer_data->ep_resolv.resolv_fail_count++;
+    } else
+        peer_data->ep_resolv.resolv_fail_count = 0;
+
+    if (!after_failure)
+        return RETRY_IN_MSEC_MAX;
+
+    if (peer_data->ep_resolv.resolv_fail_count > 20)
+        return RETRY_IN_MSEC_MAX;
+
+    /* double the retry-time, starting with one second. */
+    return NM_MIN(RETRY_IN_MSEC_MAX, (1u << peer_data->ep_resolv.resolv_fail_count) * 500);
+}
+
+static void
+_peers_resolve_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceWireGuard *       self;
+    NMDeviceWireGuardPrivate *priv;
+    PeerData *                peer_data;
+    gs_free_error GError *resolv_error = NULL;
+    GList *               list;
+    gboolean              changed = FALSE;
+    NMSockAddrUnion       sockaddr;
+    gint64                retry_in_msec;
+    char                  s_sockaddr[100];
+    char                  s_retry[100];
+
+    list = g_resolver_lookup_by_name_finish(G_RESOLVER(source_object), res, &resolv_error);
+
+    if (nm_utils_error_is_cancelled(resolv_error))
+        return;
+
+    peer_data = user_data;
+    self      = peer_data->self;
+    priv      = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    if (nm_clear_g_object(&peer_data->ep_resolv.cancellable))
+        _peers_resolving_cnt_decrement(self);
+
+    nm_assert((!resolv_error) != (!list));
+    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
+
+#define _retry_in_msec_to_string(retry_in_msec, s_retry)                               \
+    ({                                                                                 \
+        gint64 _retry_in_msec = (retry_in_msec);                                       \
+                                                                                       \
+        _retry_in_msec == RETRY_IN_MSEC_ASAP                                           \
+            ? "right away"                                                             \
+            : nm_sprintf_buf(s_retry, "in %" G_GINT64_FORMAT " msec", _retry_in_msec); \
+    })
+
+    if (resolv_error
+        && !g_error_matches(resolv_error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND)) {
+        retry_in_msec = _peers_retry_in_msec(peer_data, TRUE);
+
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: failure to resolve endpoint \"%s\": %s (retry %s)",
+              nm_wireguard_peer_get_public_key(peer_data->peer),
+              nm_wireguard_peer_get_endpoint(peer_data->peer),
+              resolv_error->message,
+              _retry_in_msec_to_string(retry_in_msec, s_retry));
+
+        _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec);
+        return;
+    }
+
+    sockaddr = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC;
+
+    if (!resolv_error) {
+        GList *iter;
+
+        for (iter = list; iter; iter = iter->next) {
+            GInetAddress *a = iter->data;
+            GSocketFamily f = g_inet_address_get_family(a);
+
+            if (f == G_SOCKET_FAMILY_IPV4) {
+                nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in_addr));
+                sockaddr.in = (struct sockaddr_in){
+                    .sin_family = AF_INET,
+                    .sin_port   = htons(nm_sock_addr_endpoint_get_port(
+                        _nm_wireguard_peer_get_endpoint(peer_data->peer))),
+                };
+                memcpy(&sockaddr.in.sin_addr, g_inet_address_to_bytes(a), sizeof(struct in_addr));
+                break;
+            }
+            if (f == G_SOCKET_FAMILY_IPV6) {
+                nm_assert(g_inet_address_get_native_size(a) == sizeof(struct in6_addr));
+                sockaddr.in6 = (struct sockaddr_in6){
+                    .sin6_family   = AF_INET6,
+                    .sin6_port     = htons(nm_sock_addr_endpoint_get_port(
+                        _nm_wireguard_peer_get_endpoint(peer_data->peer))),
+                    .sin6_scope_id = 0,
+                    .sin6_flowinfo = 0,
+                };
+                memcpy(&sockaddr.in6.sin6_addr,
+                       g_inet_address_to_bytes(a),
+                       sizeof(struct in6_addr));
+                break;
+            }
+        }
+
+        g_list_free_full(list, g_object_unref);
+    }
+
+    if (sockaddr.sa.sa_family == AF_UNSPEC) {
+        /* we failed to resolve the name. There is no need to reset the previous
+         * sockaddr. Either it was already AF_UNSPEC, or we had a good name
+         * from resolving before. In that case, we don't want to throw away
+         * a possibly good IP address, since WireGuard supports automatic roaming
+         * anyway. Either the IP address is still good (and we would wrongly
+         * reject it), or it isn't -- in which case it does not hurt much. */
+    } else {
+        if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) != 0)
+            changed = TRUE;
+        peer_data->ep_resolv.sockaddr = sockaddr;
+    }
+
+    if (resolv_error || peer_data->ep_resolv.sockaddr.sa.sa_family == AF_UNSPEC) {
+        /* while it technically did not fail, something is probably odd. Retry frequently to
+         * resolve the name, like we would do for normal failures. */
+        retry_in_msec = _peers_retry_in_msec(peer_data, TRUE);
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: no %sresults for endpoint \"%s\" (retry %s)",
+              nm_wireguard_peer_get_public_key(peer_data->peer),
+              resolv_error ? "" : "suitable ",
+              nm_wireguard_peer_get_endpoint(peer_data->peer),
+              _retry_in_msec_to_string(retry_in_msec, s_retry));
+    } else {
+        retry_in_msec = _peers_retry_in_msec(peer_data, FALSE);
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: endpoint \"%s\" resolved to %s (retry %s)",
+              nm_wireguard_peer_get_public_key(peer_data->peer),
+              nm_wireguard_peer_get_endpoint(peer_data->peer),
+              nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr,
+                                           s_sockaddr,
+                                           sizeof(s_sockaddr)),
+              _retry_in_msec_to_string(retry_in_msec, s_retry));
+    }
+
+    _peers_resolve_retry_reschedule_for_peer(self, peer_data, retry_in_msec);
+
+    if (changed) {
+        /* schedule the job in the background, to give multiple resolve events time
+         * to complete. */
+        nm_clear_g_source(&priv->link_config_delayed_id);
+        priv->link_config_delayed_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE + 1,
+                                                       link_config_delayed_resolver_cb,
+                                                       self,
+                                                       NULL);
+    }
+}
+
+static void
+_peers_resolve_start(NMDeviceWireGuard *self, PeerData *peer_data)
+{
+    NMDeviceWireGuardPrivate *priv      = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gs_unref_object GResolver *resolver = NULL;
+    const char *               host;
+
+    resolver = g_resolver_get_default();
+
+    nm_assert(!peer_data->ep_resolv.cancellable);
+
+    peer_data->ep_resolv.cancellable = g_cancellable_new();
+    priv->peers_resolving_cnt++;
+
+    /* set a special next-try timestamp. It is positive, and indicates
+     * that we are in the process of trying.
+     * This timestamp however already lies in the past, but that is correct,
+     * because we are currently in the process of trying. We will determine
+     * a next-try timestamp once the try completes. */
+    peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_PAST;
+
+    host = nm_sock_addr_endpoint_get_host(_nm_wireguard_peer_get_endpoint(peer_data->peer));
+
+    g_resolver_lookup_by_name_async(resolver,
+                                    host,
+                                    peer_data->ep_resolv.cancellable,
+                                    _peers_resolve_cb,
+                                    peer_data);
+
+    _LOGT(LOGD_DEVICE,
+          "wireguard-peer[%s]: resolving name \"%s\" for endpoint \"%s\"...",
+          nm_wireguard_peer_get_public_key(peer_data->peer),
+          host,
+          nm_wireguard_peer_get_endpoint(peer_data->peer));
+
+    nm_assert(_peers_resolving_cnt(priv) == priv->peers_resolving_cnt);
+}
+
+static void
+_peers_resolve_reresolve_all(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    PeerData *                peer_data;
+
+    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
+        if (peer_data->ep_resolv.cancellable) {
+            /* remember to retry when the currently ongoing request completes. */
+            peer_data->ep_resolv.next_try_at_nsec = NEXT_TRY_AT_NSEC_ASAP;
+        } else if (peer_data->ep_resolv.next_try_at_nsec <= 0) {
+            /* this peer does not require resolving the name. Skip it. */
+        } else {
+            /* we have a next-try scheduled. Restart right away. */
+            peer_data->ep_resolv.resolv_fail_count = 0;
+            _peers_resolve_start(self, peer_data);
+        }
+    }
+}
+
+static gboolean
+_peers_update(NMDeviceWireGuard *self,
+              PeerData *         peer_data,
+              NMWireGuardPeer *  peer,
+              gboolean           force_update)
+{
+    nm_auto_unref_wgpeer NMWireGuardPeer *old_peer = NULL;
+    NMSockAddrEndpoint *                  old_endpoint;
+    NMSockAddrEndpoint *                  endpoint;
+    gboolean                              endpoint_changed = FALSE;
+    gboolean                              changed;
+    NMSockAddrUnion                       sockaddr;
+    gboolean                              sockaddr_fixed;
+    char                                  sockaddr_sbuf[100];
+
+    nm_assert(peer);
+    nm_assert(nm_wireguard_peer_is_sealed(peer));
+
+    if (peer == peer_data->peer && !force_update)
+        return FALSE;
+
+    changed = (nm_wireguard_peer_cmp(peer, peer_data->peer, NM_SETTING_COMPARE_FLAG_EXACT) != 0);
+
+    old_peer        = peer_data->peer;
+    peer_data->peer = nm_wireguard_peer_ref(peer);
+
+    old_endpoint = old_peer ? _nm_wireguard_peer_get_endpoint(old_peer) : NULL;
+    endpoint     = peer ? _nm_wireguard_peer_get_endpoint(peer) : NULL;
+
+    endpoint_changed = (endpoint != old_endpoint
+                        && (!old_endpoint || !endpoint
+                            || !nm_streq(nm_sock_addr_endpoint_get_endpoint(old_endpoint),
+                                         nm_sock_addr_endpoint_get_endpoint(endpoint))));
+
+    if (!force_update && !endpoint_changed) {
+        /* nothing to do. */
+        return changed;
+    }
+
+    sockaddr       = (NMSockAddrUnion) NM_SOCK_ADDR_UNION_INIT_UNSPEC;
+    sockaddr_fixed = TRUE;
+    if (endpoint && nm_sock_addr_endpoint_get_host(endpoint)) {
+        if (!nm_sock_addr_endpoint_get_fixed_sockaddr(endpoint, &sockaddr)) {
+            /* we have an endpoint, but it's not a static IP address. We need to resolve
+             * the names. */
+            sockaddr_fixed = FALSE;
+        }
+    }
+
+    if (nm_sock_addr_union_cmp(&peer_data->ep_resolv.sockaddr, &sockaddr) != 0)
+        changed = TRUE;
+
+    if (nm_clear_g_cancellable(&peer_data->ep_resolv.cancellable))
+        _peers_resolving_cnt_decrement(self);
+
+    peer_data->ep_resolv = (PeerEndpointResolveData){
+        .sockaddr          = sockaddr,
+        .resolv_fail_count = 0,
+        .cancellable       = NULL,
+        .next_try_at_nsec  = 0,
+    };
+
+    if (!endpoint) {
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: no endpoint configured",
+              nm_wireguard_peer_get_public_key(peer_data->peer));
+    } else if (!nm_sock_addr_endpoint_get_host(endpoint)) {
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: invalid endpoint \"%s\"",
+              nm_wireguard_peer_get_public_key(peer_data->peer),
+              nm_sock_addr_endpoint_get_endpoint(endpoint));
+    } else if (sockaddr_fixed) {
+        _LOGT(LOGD_DEVICE,
+              "wireguard-peer[%s]: fixed endpoint \"%s\" (%s)",
+              nm_wireguard_peer_get_public_key(peer_data->peer),
+              nm_sock_addr_endpoint_get_endpoint(endpoint),
+              nm_sock_addr_union_to_string(&peer_data->ep_resolv.sockaddr,
+                                           sockaddr_sbuf,
+                                           sizeof(sockaddr_sbuf)));
+    } else
+        _peers_resolve_start(self, peer_data);
+
+    return changed;
+}
+
+static void
+_peers_remove_all(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    PeerData *                peer_data;
+
+    while ((peer_data = c_list_first_entry(&priv->lst_peers_head, PeerData, lst_peers)))
+        _peers_remove(self, peer_data);
+}
+
+static void
+_peers_update_all(NMDeviceWireGuard *self, NMSettingWireGuard *s_wg, gboolean *out_peers_removed)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    PeerData *                peer_data_safe;
+    PeerData *                peer_data;
+    guint                     i, n;
+    gboolean                  peers_removed = FALSE;
+
+    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers)
+        peer_data->dirty_update_all = TRUE;
+
+    n = nm_setting_wireguard_get_peers_len(s_wg);
+    for (i = 0; i < n; i++) {
+        NMWireGuardPeer *peer  = nm_setting_wireguard_get_peer(s_wg, i);
+        gboolean         added = FALSE;
+
+        peer_data = _peers_find(priv, peer);
+        if (!peer_data) {
+            peer_data = _peers_add(self, peer);
+            added     = TRUE;
+        }
+        _peers_update(self, peer_data, peer, added);
+        peer_data->dirty_update_all = FALSE;
+    }
+
+    c_list_for_each_entry_safe (peer_data, peer_data_safe, &priv->lst_peers_head, lst_peers) {
+        if (peer_data->dirty_update_all) {
+            _peers_remove(self, peer_data);
+            peers_removed = TRUE;
+        }
+    }
+
+    NM_SET_OUT(out_peers_removed, peers_removed);
+}
+
+static void
+_peers_get_platform_list(NMDeviceWireGuardPrivate *           priv,
+                         LinkConfigMode                       config_mode,
+                         NMPWireGuardPeer **                  out_peers,
+                         NMPlatformWireGuardChangePeerFlags **out_peer_flags,
+                         guint *                              out_len,
+                         GArray **                            out_allowed_ips_data)
+{
+    gs_free NMPWireGuardPeer *plpeers                        = NULL;
+    gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL;
+    gs_unref_array GArray *allowed_ips                       = NULL;
+    PeerData *             peer_data;
+    guint                  i_good;
+    guint                  n_aip;
+    guint                  i_aip;
+    guint                  len;
+    guint                  i;
+
+    nm_assert(out_peers && !*out_peers);
+    nm_assert(out_peer_flags && !*out_peer_flags);
+    nm_assert(out_len && *out_len == 0);
+    nm_assert(out_allowed_ips_data && !*out_allowed_ips_data);
+
+    len = g_hash_table_size(priv->peers);
+
+    nm_assert(len == c_list_length(&priv->lst_peers_head));
+
+    if (len == 0)
+        return;
+
+    plpeers      = g_new0(NMPWireGuardPeer, len);
+    plpeer_flags = g_new0(NMPlatformWireGuardChangePeerFlags, len);
+
+    i_good = 0;
+    c_list_for_each_entry (peer_data, &priv->lst_peers_head, lst_peers) {
+        NMPlatformWireGuardChangePeerFlags *plf = &plpeer_flags[i_good];
+        NMPWireGuardPeer *                  plp = &plpeers[i_good];
+        NMSettingSecretFlags                psk_secret_flags;
+
+        if (!nm_utils_base64secret_decode(nm_wireguard_peer_get_public_key(peer_data->peer),
+                                          sizeof(plp->public_key),
+                                          plp->public_key))
+            continue;
+
+        *plf = NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_NONE;
+
+        plp->persistent_keepalive_interval =
+            nm_wireguard_peer_get_persistent_keepalive(peer_data->peer);
+        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY))
+            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_KEEPALIVE_INTERVAL;
+
+        /* if the peer has an endpoint but it is not yet resolved (not ready),
+         * we still configure it and leave the endpoint unspecified. Later,
+         * when we can resolve the endpoint, we will update. */
+        plp->endpoint = peer_data->ep_resolv.sockaddr;
+        if (plp->endpoint.sa.sa_family == AF_UNSPEC) {
+            /* we don't actually ever clear endpoints, if we don't have better information. */
+        } else
+            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ENDPOINT;
+
+        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) {
+            psk_secret_flags = nm_wireguard_peer_get_preshared_key_flags(peer_data->peer);
+            if (!NM_FLAGS_HAS(psk_secret_flags, NM_SETTING_SECRET_FLAG_NOT_REQUIRED)) {
+                if (!nm_utils_base64secret_decode(
+                        nm_wireguard_peer_get_preshared_key(peer_data->peer),
+                        sizeof(plp->preshared_key),
+                        plp->preshared_key)
+                    && config_mode == LINK_CONFIG_MODE_FULL)
+                    goto skip;
+            }
+            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_PRESHARED_KEY;
+        }
+
+        if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)
+            && ((n_aip = nm_wireguard_peer_get_allowed_ips_len(peer_data->peer)) > 0)) {
+            if (!allowed_ips)
+                allowed_ips = g_array_new(FALSE, FALSE, sizeof(NMPWireGuardAllowedIP));
+
+            *plf |= NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_HAS_ALLOWEDIPS
+                    | NM_PLATFORM_WIREGUARD_CHANGE_PEER_FLAG_REPLACE_ALLOWEDIPS;
+
+            plp->_construct_idx_start = allowed_ips->len;
+            for (i_aip = 0; i_aip < n_aip; i_aip++) {
+                const char *aip;
+                NMIPAddr    addrbin = {};
+                int         addr_family;
+                gboolean    valid;
+                int         prefix;
+
+                aip = nm_wireguard_peer_get_allowed_ip(peer_data->peer, i_aip, &valid);
+                if (!valid
+                    || !nm_utils_parse_inaddr_prefix_bin(AF_UNSPEC,
+                                                         aip,
+                                                         &addr_family,
+                                                         &addrbin,
+                                                         &prefix)) {
+                    /* the address is really not expected to be invalid, because then
+                     * the connection would not verify. Anyway, silently skip it. */
+                    continue;
+                }
+
+                if (prefix == -1)
+                    prefix = addr_family == AF_INET ? 32 : 128;
+
+                g_array_append_val(allowed_ips,
+                                   ((NMPWireGuardAllowedIP){
+                                       .family = addr_family,
+                                       .mask   = prefix,
+                                       .addr   = addrbin,
+                                   }));
+            }
+            plp->_construct_idx_end = allowed_ips->len;
+        }
+
+        i_good++;
+        continue;
+
+skip:
+        memset(plp, 0, sizeof(*plp));
+    }
+
+    if (i_good == 0)
+        return;
+
+    for (i = 0; i < i_good; i++) {
+        NMPWireGuardPeer *plp = &plpeers[i];
+        guint             l;
+
+        if (plp->_construct_idx_end == 0) {
+            nm_assert(plp->_construct_idx_start == 0);
+            plp->allowed_ips     = NULL;
+            plp->allowed_ips_len = 0;
+        } else {
+            nm_assert(plp->_construct_idx_start < plp->_construct_idx_end);
+            l = plp->_construct_idx_end - plp->_construct_idx_start;
+            plp->allowed_ips =
+                &g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start);
+            plp->allowed_ips_len = l;
+        }
+    }
+    *out_peers            = g_steal_pointer(&plpeers);
+    *out_peer_flags       = g_steal_pointer(&plpeer_flags);
+    *out_len              = i_good;
+    *out_allowed_ips_data = g_steal_pointer(&allowed_ips);
+}
+
+/*****************************************************************************/
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceWireGuard *           self;
+    NMDeviceWireGuardPrivate *    priv;
+    const NMPlatformLink *        plink;
+    const NMPlatformLnkWireGuard *props = NULL;
+    int                           ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE_WIREGUARD(device));
+    self = NM_DEVICE_WIREGUARD(device);
+    priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    ifindex = nm_device_get_ifindex(device);
+    props   = nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device), ifindex, &plink);
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get wireguard properties");
+        return;
+    }
+
+    g_object_freeze_notify(G_OBJECT(device));
+
+#define CHECK_PROPERTY_CHANGED(field, prop)         \
+    G_STMT_START                                    \
+    {                                               \
+        if (priv->lnk_curr.field != props->field) { \
+            priv->lnk_curr.field = props->field;    \
+            _notify(self, prop);                    \
+        }                                           \
+    }                                               \
+    G_STMT_END
+
+#define CHECK_PROPERTY_CHANGED_ARRAY(field, prop)                                              \
+    G_STMT_START                                                                               \
+    {                                                                                          \
+        if (memcmp(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field)) != 0) { \
+            memcpy(&priv->lnk_curr.field, &props->field, sizeof(priv->lnk_curr.field));        \
+            _notify(self, prop);                                                               \
+        }                                                                                      \
+    }                                                                                          \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED_ARRAY(public_key, PROP_PUBLIC_KEY);
+    CHECK_PROPERTY_CHANGED(listen_port, PROP_LISTEN_PORT);
+    CHECK_PROPERTY_CHANGED(fwmark, PROP_FWMARK);
+
+    g_object_thaw_notify(G_OBJECT(device));
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *iface = nm_device_get_iface(device);
+    int         r;
+
+    g_return_val_if_fail(iface, FALSE);
+
+    r = nm_platform_link_wireguard_add(nm_device_get_platform(device), iface, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create WireGuard interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_secrets_cancel(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    if (priv->secrets_call_id)
+        nm_act_request_cancel_secrets(NULL, priv->secrets_call_id);
+    nm_assert(!priv->secrets_call_id);
+}
+
+static void
+_secrets_cb(NMActRequest *                req,
+            NMActRequestGetSecretsCallId *call_id,
+            NMSettingsConnection *        connection,
+            GError *                      error,
+            gpointer                      user_data)
+{
+    NMDeviceWireGuard *       self   = NM_DEVICE_WIREGUARD(user_data);
+    NMDevice *                device = NM_DEVICE(self);
+    NMDeviceWireGuardPrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE_WIREGUARD(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->secrets_call_id == call_id);
+
+    priv->secrets_call_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == connection);
+
+    if (error) {
+        _LOGW(LOGD_ETHER, "%s", error->message);
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+_secrets_get_secrets(NMDeviceWireGuard *          self,
+                     const char *                 setting_name,
+                     NMSecretAgentGetSecretsFlags flags,
+                     const char *const *          hints)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    NMActRequest *            req;
+
+    _secrets_cancel(self);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv->secrets_call_id =
+        nm_act_request_get_secrets(req, TRUE, setting_name, flags, hints, _secrets_cb, self);
+    g_return_if_fail(priv->secrets_call_id);
+}
+
+static NMActStageReturn
+_secrets_handle_auth_or_fail(NMDeviceWireGuard *self, NMActRequest *req, gboolean new_secrets)
+{
+    NMConnection *    applied_connection;
+    const char *      setting_name;
+    gs_unref_ptrarray GPtrArray *hints = NULL;
+
+    if (!nm_device_auth_retries_try_next(NM_DEVICE(self)))
+        return NM_ACT_STAGE_RETURN_FAILURE;
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_NONE);
+
+    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(req));
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    setting_name       = nm_connection_need_secrets(applied_connection, &hints);
+    if (!setting_name) {
+        _LOGI(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets.");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (hints)
+        g_ptr_array_add(hints, NULL);
+
+    _secrets_get_secrets(self,
+                         setting_name,
+                         NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+                             | (new_secrets ? NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW : 0),
+                         (hints ? (const char *const *) hints->pdata : NULL));
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static void
+_dns_config_changed(NMDnsManager *dns_manager, NMDeviceWireGuard *self)
+{
+    /* when the DNS configuration changes, we re-resolve the peer addresses.
+     *
+     * Possibly, we should also do that when the default-route changes, but it's
+     * hard to figure out when that happens. */
+    _peers_resolve_reresolve_all(self);
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+link_config(NMDeviceWireGuard *  self,
+            const char *         reason,
+            LinkConfigMode       config_mode,
+            NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWireGuardPrivate *           priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    nm_auto_bzero_secret_ptr NMSecretPtr wg_lnk_clear_private_key = NM_SECRET_PTR_INIT();
+    NMSettingWireGuard *                 s_wg;
+    NMConnection *                       connection;
+    NMActStageReturn                     ret;
+    gs_unref_array GArray *allowed_ips_data = NULL;
+    NMPlatformLnkWireGuard wg_lnk;
+    gs_free NMPWireGuardPeer *plpeers                        = NULL;
+    gs_free NMPlatformWireGuardChangePeerFlags *plpeer_flags = NULL;
+    guint                                       plpeers_len  = 0;
+    const char *                                setting_name;
+    gboolean                                    peers_removed;
+    NMPlatformWireGuardChangeFlags              wg_change_flags;
+    int                                         ifindex;
+    int                                         r;
+
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE);
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+    s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD));
+    g_return_val_if_fail(s_wg, NM_ACT_STAGE_RETURN_FAILURE);
+
+    priv->link_config_last_at = nm_utils_get_monotonic_timestamp_nsec();
+
+    _LOGT(LOGD_DEVICE,
+          "wireguard link config (%s, %s)...",
+          reason,
+          _link_config_mode_to_string(config_mode));
+
+    _auto_default_route_init(self);
+
+    if (!priv->dns_manager) {
+        priv->dns_manager = g_object_ref(nm_dns_manager_get());
+        g_signal_connect(priv->dns_manager,
+                         NM_DNS_MANAGER_CONFIG_CHANGED,
+                         G_CALLBACK(_dns_config_changed),
+                         self);
+    }
+
+    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)
+        && (setting_name = nm_connection_need_secrets(connection, NULL))) {
+        NMActRequest *req = nm_device_get_act_request(NM_DEVICE(self));
+
+        _LOGD(LOGD_DEVICE,
+              "Activation: connection '%s' has security, but secrets are required.",
+              nm_connection_get_id(connection));
+
+        ret = _secrets_handle_auth_or_fail(self, req, FALSE);
+        if (ret != NM_ACT_STAGE_RETURN_SUCCESS) {
+            if (ret != NM_ACT_STAGE_RETURN_POSTPONE) {
+                nm_assert(ret == NM_ACT_STAGE_RETURN_FAILURE);
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+            }
+            return ret;
+        }
+    }
+
+    ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+    if (ifindex <= 0) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    _peers_update_all(self, s_wg, &peers_removed);
+
+    wg_lnk = (NMPlatformLnkWireGuard){};
+
+    wg_change_flags = NM_PLATFORM_WIREGUARD_CHANGE_FLAG_NONE;
+
+    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)
+        || (NM_IN_SET(config_mode, LINK_CONFIG_MODE_REAPPLY) && peers_removed))
+        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_REPLACE_PEERS;
+
+    if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL, LINK_CONFIG_MODE_REAPPLY)) {
+        wg_lnk.listen_port = nm_setting_wireguard_get_listen_port(s_wg);
+        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_LISTEN_PORT;
+
+        wg_lnk.fwmark = priv->auto_default_route_fwmark;
+        wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_FWMARK;
+
+        if (nm_utils_base64secret_decode(nm_setting_wireguard_get_private_key(s_wg),
+                                         sizeof(wg_lnk.private_key),
+                                         wg_lnk.private_key)) {
+            wg_lnk_clear_private_key = NM_SECRET_PTR_ARRAY(wg_lnk.private_key);
+            wg_change_flags |= NM_PLATFORM_WIREGUARD_CHANGE_FLAG_HAS_PRIVATE_KEY;
+        } else {
+            if (NM_IN_SET(config_mode, LINK_CONFIG_MODE_FULL)) {
+                _LOGD(LOGD_DEVICE, "the provided private-key is invalid");
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+                return NM_ACT_STAGE_RETURN_FAILURE;
+            }
+        }
+    }
+
+    _peers_get_platform_list(priv,
+                             config_mode,
+                             &plpeers,
+                             &plpeer_flags,
+                             &plpeers_len,
+                             &allowed_ips_data);
+
+    r = nm_platform_link_wireguard_change(nm_device_get_platform(NM_DEVICE(self)),
+                                          ifindex,
+                                          &wg_lnk,
+                                          plpeers,
+                                          plpeer_flags,
+                                          plpeers_len,
+                                          wg_change_flags);
+
+    nm_explicit_bzero(plpeers, sizeof(plpeers[0]) * plpeers_len);
+
+    if (r < 0) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+link_config_delayed(NMDeviceWireGuard *self, const char *reason)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gint64                    now;
+
+    priv->link_config_delayed_id = 0;
+
+    if (priv->link_config_last_at != 0) {
+        now = nm_utils_get_monotonic_timestamp_nsec();
+        if (now < priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC) {
+            /* we ratelimit calls to link_config(), because we call this whenever a resolver
+             * completes. */
+            _LOGT(LOGD_DEVICE, "wireguard link config (%s) (postponed)", reason);
+            priv->link_config_delayed_id =
+                g_timeout_add(NM_MAX((priv->link_config_last_at + LINK_CONFIG_RATE_LIMIT_NSEC - now)
+                                         / NM_UTILS_NSEC_PER_MSEC,
+                                     (gint64) 1),
+                              link_config_delayed_ratelimit_cb,
+                              self);
+            return;
+        }
+    }
+
+    link_config(self, reason, LINK_CONFIG_MODE_ENDPOINTS, NULL);
+}
+
+static gboolean
+link_config_delayed_ratelimit_cb(gpointer user_data)
+{
+    link_config_delayed(user_data, "after-ratelimiting");
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+link_config_delayed_resolver_cb(gpointer user_data)
+{
+    link_config_delayed(user_data, "resolver-update");
+    return G_SOURCE_REMOVE;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWireGuard *       self = NM_DEVICE_WIREGUARD(device);
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    NMDeviceSysIfaceState     sys_iface_state;
+    NMDeviceStateReason       failure_reason;
+    NMActStageReturn          ret;
+
+    sys_iface_state = nm_device_sys_iface_state_get(device);
+
+    if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NONE);
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    ret =
+        link_config(NM_DEVICE_WIREGUARD(device),
+                    "configure",
+                    (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) ? LINK_CONFIG_MODE_ASSUME
+                                                                          : LINK_CONFIG_MODE_FULL,
+                    &failure_reason);
+
+    if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
+        if (sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_ASSUME) {
+            /* this never fails. */
+            return NM_ACT_STAGE_RETURN_SUCCESS;
+        }
+
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, failure_reason);
+        NM_SET_OUT(out_failure_reason, failure_reason);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    nm_assert(NM_IN_SET(ret, NM_ACT_STAGE_RETURN_SUCCESS, NM_ACT_STAGE_RETURN_POSTPONE));
+
+    if (ret == NM_ACT_STAGE_RETURN_SUCCESS && _peers_resolving_cnt(priv) > 0u) {
+        _LOGT(LOGD_DEVICE,
+              "activation delayed to resolve DNS names of peers: resolving and waiting...");
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    return ret;
+}
+
+static NMIPConfig *
+_get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
+{
+    NMDeviceWireGuardPrivate *priv        = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gs_unref_object NMIPConfig *ip_config = NULL;
+    NMConnection *              connection;
+    NMSettingWireGuard *        s_wg;
+    guint                       n_peers;
+    guint                       i;
+    int                         ip_ifindex;
+    guint32                     route_metric;
+    guint32                     route_table_coerced;
+    gboolean                    auto_default_route_enabled;
+
+    _auto_default_route_init(self);
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    s_wg = NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD));
+
+    /* Differences to `wg-quick`.
+     *
+     * `wg-quick` supports the "Table" setting with 3 modes:
+     *
+     * a1) "off": this is what we do with "peer-routes" disabled.
+     *
+     * a2) an explicit routing table. This is our behavior with "peer-routes" on. In this case
+     *   we honor the "ipv4.route-table" and "ipv6.route-table" settings. One difference is that
+     *   `wg-quick` would resolve table names from /etc/iproute2/rt_tables. Our connection profiles
+     *   only contain table numbers, so that conversion from name to table must have happened
+     *   before already.
+     *
+     * a3) "auto" (the default). In this case, `wg-quick` would only add the route to the
+     *   main table, if the AllowedIP range is not yet reachable on the link. With "peer-routes"
+     *   enabled, we don't check for that and always add the routes to the main-table
+     *   (with 'ipv4.route-table' and 'ipv6.route-table' set to zero or RT_TABLE_MAIN (254)).
+     *
+     *   Also, in "auto" mode, `wg-quick` would add special handling for /0 routes and pick
+     *   an empty table to configure policy routing to avoid routing loops. This handling
+     *   of routing-loops via policy routing is not yet done, and requires a separate solution
+     *   from constructing the peer-routes here.
+     */
+    if (!nm_setting_wireguard_get_peer_routes(s_wg))
+        return NULL;
+
+    ip_ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
+
+    if (ip_ifindex <= 0)
+        return NULL;
+
+    route_metric = nm_device_get_route_metric(NM_DEVICE(self), addr_family);
+
+    route_table_coerced =
+        nm_platform_route_table_coerce(nm_device_get_route_table(NM_DEVICE(self), addr_family));
+
+    auto_default_route_enabled = (addr_family == AF_INET) ? priv->auto_default_route_enabled_4
+                                                          : priv->auto_default_route_enabled_6;
+
+    n_peers = nm_setting_wireguard_get_peers_len(s_wg);
+    for (i = 0; i < n_peers; i++) {
+        NMWireGuardPeer *peer = nm_setting_wireguard_get_peer(s_wg, i);
+        guint            n_aips;
+        guint            j;
+
+        n_aips = nm_wireguard_peer_get_allowed_ips_len(peer);
+        for (j = 0; j < n_aips; j++) {
+            NMPlatformIPXRoute rt;
+            NMIPAddr           addrbin;
+            const char *       aip;
+            gboolean           valid;
+            int                prefix;
+            guint32            rtable_coerced;
+
+            aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
+
+            if (!valid
+                || !nm_utils_parse_inaddr_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
+                continue;
+
+            if (prefix < 0)
+                prefix = (addr_family == AF_INET) ? 32 : 128;
+
+            if (prefix == 0) {
+                NMSettingIPConfig *s_ip;
+
+                s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+                if (nm_setting_ip_config_get_never_default(s_ip))
+                    continue;
+            }
+
+            if (!ip_config) {
+                ip_config = nm_device_ip_config_new(NM_DEVICE(self), addr_family);
+                nm_ip_config_set_config_flags(ip_config,
+                                              NM_IP_CONFIG_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES,
+                                              0);
+            }
+
+            nm_utils_ipx_address_clear_host_address(addr_family, &addrbin, NULL, prefix);
+
+            rtable_coerced = route_table_coerced;
+
+            if (prefix == 0 && auto_default_route_enabled) {
+                /* In auto-default-route mode, we place the default route in a table that
+                 * has the same number as the fwmark. wg-quick does that too. If you don't
+                 * like that, configure the rules and the default-route explicitly in the
+                 * connection profile. */
+                rtable_coerced = nm_platform_route_table_coerce(priv->auto_default_route_fwmark);
+            }
+
+            if (addr_family == AF_INET) {
+                rt.r4 = (NMPlatformIP4Route){
+                    .network       = addrbin.addr4,
+                    .plen          = prefix,
+                    .ifindex       = ip_ifindex,
+                    .rt_source     = NM_IP_CONFIG_SOURCE_USER,
+                    .table_coerced = rtable_coerced,
+                    .metric        = route_metric,
+                };
+            } else {
+                rt.r6 = (NMPlatformIP6Route){
+                    .network       = addrbin.addr6,
+                    .plen          = prefix,
+                    .ifindex       = ip_ifindex,
+                    .rt_source     = NM_IP_CONFIG_SOURCE_USER,
+                    .table_coerced = rtable_coerced,
+                    .metric        = route_metric,
+                };
+            }
+
+            nm_ip_config_add_route(ip_config, &rt.rx, NULL);
+        }
+    }
+
+    return g_steal_pointer(&ip_config);
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    gs_unref_object NMIPConfig *ip_config = NULL;
+
+    ip_config = _get_dev2_ip_config(NM_DEVICE_WIREGUARD(device), addr_family);
+
+    nm_device_set_dev2_ip_config(device, addr_family, ip_config);
+
+    return NM_DEVICE_CLASS(nm_device_wireguard_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    /* When "MTU" for `wg-quick up` is unset, it calls `ip route get` for
+     * each configured endpoint, to determine the suitable MTU how to reach
+     * each endpoint.
+     * For `wg-quick` this works very well, because whenever the script runs it
+     * determines the best setting at that point in time. It's simply not concerned
+     * with what happens later (and it's not around anyway).
+     *
+     * NetworkManager sticks around, so the right MTU would need to be re-determined
+     * whenever anything relevant changes. Which basically means, to re-evaluate whenever
+     * something related to addresses or routing changes (which happens all the time).
+     *
+     * The correct MTU indeed depends on the MTU setting of other interfaces (or routes).
+     * But it's still odd, that activating/deactivating a seemingly unrelated interface
+     * would trigger an MTU change. It's odd to explain/document and odd to implemented
+     * -- despite this being the reality.
+     *
+     * For now, only support configuring an explicit MTU, or leave the setting untouched.
+     * The same limitation also applies to other "ip-tunnel" types, where we could use
+     * similar smarts for autodetecting the MTU.
+     */
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_WIREGUARD,
+                                                        out_source);
+}
+
+static void
+_device_cleanup(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    _peers_remove_all(self);
+
+    _secrets_cancel(self);
+
+    priv->auto_default_route_initialized          = FALSE;
+    priv->auto_default_route_priority_initialized = FALSE;
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    if (new_state <= NM_DEVICE_STATE_ACTIVATED)
+        return;
+
+    _device_cleanup(NM_DEVICE_WIREGUARD(device));
+}
+
+/*****************************************************************************/
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    if (nm_streq(setting_name, NM_SETTING_WIREGUARD_SETTING_NAME)) {
+        /* Most, but not all WireGuard settings can be reapplied. Whitelist.
+         *
+         * MTU cannot be reapplied. */
+        return nm_device_hash_check_invalid_keys(diffs,
+                                                 NM_SETTING_WIREGUARD_SETTING_NAME,
+                                                 error,
+                                                 NM_SETTING_WIREGUARD_FWMARK,
+                                                 NM_SETTING_WIREGUARD_IP4_AUTO_DEFAULT_ROUTE,
+                                                 NM_SETTING_WIREGUARD_IP6_AUTO_DEFAULT_ROUTE,
+                                                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                                                 NM_SETTING_WIREGUARD_PEERS,
+                                                 NM_SETTING_WIREGUARD_PEER_ROUTES,
+                                                 NM_SETTING_WIREGUARD_PRIVATE_KEY,
+                                                 NM_SETTING_WIREGUARD_PRIVATE_KEY_FLAGS);
+    }
+
+    return NM_DEVICE_CLASS(nm_device_wireguard_parent_class)
+        ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static void
+reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceWireGuard *       self         = NM_DEVICE_WIREGUARD(device);
+    NMDeviceWireGuardPrivate *priv         = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+    gs_unref_object NMIPConfig *ip4_config = NULL;
+    gs_unref_object NMIPConfig *ip6_config = NULL;
+    NMDeviceState               state      = nm_device_get_state(device);
+
+    NM_DEVICE_CLASS(nm_device_wireguard_parent_class)->reapply_connection(device, con_old, con_new);
+
+    if (state >= NM_DEVICE_STATE_CONFIG) {
+        priv->auto_default_route_refresh = TRUE;
+        link_config(NM_DEVICE_WIREGUARD(device), "reapply", LINK_CONFIG_MODE_REAPPLY, NULL);
+    }
+
+    if (state >= NM_DEVICE_STATE_IP_CONFIG) {
+        ip4_config = _get_dev2_ip_config(self, AF_INET);
+        ip6_config = _get_dev2_ip_config(self, AF_INET6);
+
+        nm_device_set_dev2_ip_config(device, AF_INET, ip4_config);
+        nm_device_set_dev2_ip_config(device, AF_INET6, ip6_config);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(device);
+    NMSettingWireGuard *      s_wg =
+        NM_SETTING_WIREGUARD(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIREGUARD));
+    const NMPObject *            obj_wg;
+    const NMPObjectLnkWireGuard *olnk_wg;
+    guint                        i;
+
+    if (!s_wg) {
+        s_wg = NM_SETTING_WIREGUARD(nm_setting_wireguard_new());
+        nm_connection_add_setting(connection, NM_SETTING(s_wg));
+    }
+
+    g_object_set(s_wg,
+                 NM_SETTING_WIREGUARD_FWMARK,
+                 (guint) priv->lnk_curr.fwmark,
+                 NM_SETTING_WIREGUARD_LISTEN_PORT,
+                 (guint) priv->lnk_curr.listen_port,
+                 NULL);
+
+    obj_wg = NMP_OBJECT_UP_CAST(nm_platform_link_get_lnk_wireguard(nm_device_get_platform(device),
+                                                                   nm_device_get_ip_ifindex(device),
+                                                                   NULL));
+    if (!obj_wg)
+        return;
+
+    olnk_wg = &obj_wg->_lnk_wireguard;
+
+    for (i = 0; i < olnk_wg->peers_len; i++) {
+        nm_auto_unref_wgpeer NMWireGuardPeer *peer  = NULL;
+        const NMPWireGuardPeer *              ppeer = &olnk_wg->peers[i];
+
+        peer = nm_wireguard_peer_new();
+
+        _nm_wireguard_peer_set_public_key_bin(peer, ppeer->public_key);
+
+        nm_setting_wireguard_append_peer(s_wg, peer);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceWireGuard *       self = NM_DEVICE_WIREGUARD(object);
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_PUBLIC_KEY:
+        g_value_take_variant(value,
+                             g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE,
+                                                       priv->lnk_curr.public_key,
+                                                       sizeof(priv->lnk_curr.public_key),
+                                                       1));
+        break;
+    case PROP_LISTEN_PORT:
+        g_value_set_uint(value, priv->lnk_curr.listen_port);
+        break;
+    case PROP_FWMARK:
+        g_value_set_uint(value, priv->lnk_curr.fwmark);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_wireguard_init(NMDeviceWireGuard *self)
+{
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    c_list_init(&priv->lst_peers_head);
+    priv->peers = g_hash_table_new(_peer_data_hash, _peer_data_equal);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceWireGuard *self = NM_DEVICE_WIREGUARD(object);
+
+    _device_cleanup(self);
+
+    G_OBJECT_CLASS(nm_device_wireguard_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceWireGuard *       self = NM_DEVICE_WIREGUARD(object);
+    NMDeviceWireGuardPrivate *priv = NM_DEVICE_WIREGUARD_GET_PRIVATE(self);
+
+    nm_explicit_bzero(priv->lnk_curr.private_key, sizeof(priv->lnk_curr.private_key));
+
+    if (priv->dns_manager) {
+        g_signal_handlers_disconnect_by_func(priv->dns_manager, _dns_config_changed, self);
+        g_object_unref(priv->dns_manager);
+    }
+
+    g_hash_table_destroy(priv->peers);
+
+    G_OBJECT_CLASS(nm_device_wireguard_parent_class)->finalize(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_wireguard = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_WIREGUARD,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("PublicKey",
+                                                           "ay",
+                                                           NM_DEVICE_WIREGUARD_PUBLIC_KEY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ListenPort",
+                                                           "q",
+                                                           NM_DEVICE_WIREGUARD_LISTEN_PORT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FwMark",
+                                                           "u",
+                                                           NM_DEVICE_WIREGUARD_FWMARK), ), ),
+};
+
+static void
+nm_device_wireguard_class_init(NMDeviceWireGuardClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wireguard);
+
+    device_class->connection_type_supported        = NM_SETTING_WIREGUARD_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_WIREGUARD_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD);
+
+    device_class->state_changed                                 = device_state_changed;
+    device_class->create_and_realize                            = create_and_realize;
+    device_class->act_stage2_config                             = act_stage2_config;
+    device_class->act_stage2_config_also_for_external_or_assume = TRUE;
+    device_class->act_stage3_ip_config_start                    = act_stage3_ip_config_start;
+    device_class->get_generic_capabilities                      = get_generic_capabilities;
+    device_class->link_changed                                  = link_changed;
+    device_class->update_connection                             = update_connection;
+    device_class->can_reapply_change                            = can_reapply_change;
+    device_class->reapply_connection                            = reapply_connection;
+    device_class->get_configured_mtu                            = get_configured_mtu;
+    device_class->get_extra_rules                               = get_extra_rules;
+    device_class->coerce_route_table                            = coerce_route_table;
+
+    obj_properties[PROP_PUBLIC_KEY] =
+        g_param_spec_variant(NM_DEVICE_WIREGUARD_PUBLIC_KEY,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("ay"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LISTEN_PORT] = g_param_spec_uint(NM_DEVICE_WIREGUARD_LISTEN_PORT,
+                                                         "",
+                                                         "",
+                                                         0,
+                                                         G_MAXUINT16,
+                                                         0,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_FWMARK] = g_param_spec_uint(NM_DEVICE_WIREGUARD_FWMARK,
+                                                    "",
+                                                    "",
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*************************************************************/
+
+#define NM_TYPE_WIREGUARD_DEVICE_FACTORY (nm_wireguard_device_factory_get_type())
+#define NM_WIREGUARD_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIREGUARD_DEVICE_FACTORY, NMWireGuardDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_WIREGUARD,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "WireGuard",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_WIREGUARD,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_WIREGUARD,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    WIREGUARD,
+    WireGuard,
+    wireguard,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WIREGUARD)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIREGUARD_SETTING_NAME),
+    factory_class->create_device = create_device;)
diff --git a/src/core/devices/nm-device-wireguard.h b/src/core/devices/nm-device-wireguard.h
new file mode 100644
index 00000000..7e18bdba
--- /dev/null
+++ b/src/core/devices/nm-device-wireguard.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Javier Arteaga <jarteaga@jbeta.is>
+ */
+
+#ifndef __NM_DEVICE_WIREGUARD_H__
+#define __NM_DEVICE_WIREGUARD_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_WIREGUARD (nm_device_wireguard_get_type())
+#define NM_DEVICE_WIREGUARD(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuard))
+#define NM_DEVICE_WIREGUARD_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuardClass))
+#define NM_IS_DEVICE_WIREGUARD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIREGUARD))
+#define NM_IS_DEVICE_WIREGUARD_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIREGUARD))
+#define NM_DEVICE_WIREGUARD_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIREGUARD, NMDeviceWireGuardClass))
+
+#define NM_DEVICE_WIREGUARD_PUBLIC_KEY  "public-key"
+#define NM_DEVICE_WIREGUARD_LISTEN_PORT "listen-port"
+#define NM_DEVICE_WIREGUARD_FWMARK      "fwmark"
+
+typedef struct _NMDeviceWireGuard      NMDeviceWireGuard;
+typedef struct _NMDeviceWireGuardClass NMDeviceWireGuardClass;
+
+GType nm_device_wireguard_get_type(void);
+
+#endif /* __NM_DEVICE_WIREGUARD_H__ */
diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c
new file mode 100644
index 00000000..2f3b16ff
--- /dev/null
+++ b/src/core/devices/nm-device-wpan.c
@@ -0,0 +1,257 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-manager.h"
+#include "nm-device-wpan.h"
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <linux/if.h>
+
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "nm-ip4-config.h"
+#include "platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-wpan.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceWpan
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceWpan {
+    NMDevice parent;
+};
+
+struct _NMDeviceWpanClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceWpan, nm_device_wpan, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingWpan *s_wpan;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_WPAN_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("WPAN connection"),
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    s_wpan = NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN));
+    if (!s_wpan) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "A 'wpan' setting is required.");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMSettingWpan *s_wpan =
+        NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN));
+
+    if (!s_wpan) {
+        s_wpan = (NMSettingWpan *) nm_setting_wpan_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_wpan);
+    }
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingWpan *s_wpan;
+    const char *   mac, *hw_addr;
+
+    if (!NM_DEVICE_CLASS(nm_device_wpan_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_wpan = NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN));
+
+    mac = nm_setting_wpan_get_mac_address(s_wpan);
+    if (mac) {
+        hw_addr = nm_device_get_hw_address(device);
+        if (!nm_utils_hwaddr_matches(mac, -1, hw_addr, -1)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "MAC address mismatches");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWpan *        self = NM_DEVICE_WPAN(device);
+    NMSettingWpan *       s_wpan;
+    NMPlatform *          platform;
+    guint16               pan_id;
+    guint16               short_address;
+    gint16                page, channel;
+    int                   ifindex;
+    const guint8 *        hwaddr;
+    gsize                 hwaddr_len = 0;
+    const NMPlatformLink *lowpan_plink;
+    NMDevice *            lowpan_device = NULL;
+    NMActStageReturn      ret           = NM_ACT_STAGE_RETURN_FAILURE;
+
+    platform = nm_device_get_platform(device);
+    nm_assert(NM_IS_PLATFORM(platform));
+
+    ifindex = nm_device_get_ifindex(device);
+
+    g_return_val_if_fail(ifindex > 0, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_wpan = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WPAN);
+
+    g_return_val_if_fail(s_wpan, NM_ACT_STAGE_RETURN_FAILURE);
+
+    hwaddr = nm_platform_link_get_address(platform, ifindex, &hwaddr_len);
+
+    if (!hwaddr) {
+        *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    /* As of kernel 4.16, the 6LoWPAN devices layered on top of WPANs
+     * need to be DOWN as well as the WPAN device itself in order to
+     * modify the WPAN properties. */
+    lowpan_plink =
+        nm_platform_link_get_by_address(platform, NM_LINK_TYPE_6LOWPAN, hwaddr, hwaddr_len);
+    if (lowpan_plink && NM_FLAGS_HAS(lowpan_plink->n_ifi_flags, IFF_UP)) {
+        lowpan_device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, lowpan_plink->ifindex);
+    }
+
+    if (lowpan_device)
+        nm_device_take_down(lowpan_device, TRUE);
+
+    nm_device_take_down(device, TRUE);
+
+    pan_id = nm_setting_wpan_get_pan_id(s_wpan);
+    if (pan_id != G_MAXUINT16) {
+        if (!nm_platform_wpan_set_pan_id(platform, ifindex, pan_id)) {
+            _LOGW(LOGD_DEVICE, "unable to set the PAN ID");
+            goto out;
+        }
+    }
+
+    short_address = nm_setting_wpan_get_short_address(s_wpan);
+    if (short_address != G_MAXUINT16) {
+        if (!nm_platform_wpan_set_short_addr(platform, ifindex, short_address)) {
+            _LOGW(LOGD_DEVICE, "unable to set the short address");
+            goto out;
+        }
+    }
+
+    channel = nm_setting_wpan_get_channel(s_wpan);
+    if (channel != NM_SETTING_WPAN_CHANNEL_DEFAULT) {
+        page = nm_setting_wpan_get_page(s_wpan);
+        if (!nm_platform_wpan_set_channel(platform, ifindex, page, channel)) {
+            _LOGW(LOGD_DEVICE, "unable to set the channel");
+            goto out;
+        }
+    }
+
+    ret = NM_ACT_STAGE_RETURN_SUCCESS;
+
+out:
+    nm_device_bring_up(device, TRUE, NULL);
+
+    if (lowpan_device)
+        nm_device_bring_up(lowpan_device, TRUE, NULL);
+
+    return ret;
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_wpan_init(NMDeviceWpan *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_wpan = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_WPAN,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
+                                                           "s",
+                                                           NM_DEVICE_HW_ADDRESS), ), ),
+};
+
+static void
+nm_device_wpan_class_init(NMDeviceWpanClass *klass)
+{
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wpan);
+
+    device_class->connection_type_supported        = NM_SETTING_WPAN_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_WPAN_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WPAN);
+
+    device_class->complete_connection         = complete_connection;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->update_connection           = update_connection;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_WPAN_DEVICE_FACTORY (nm_wpan_device_factory_get_type())
+#define NM_WPAN_DEVICE_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_DEVICE_FACTORY, NMWpanDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_WPAN,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "WPAN",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_WPAN,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_WPAN,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    WPAN,
+    Wpan,
+    wpan,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WPAN)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WPAN_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-wpan.h b/src/core/devices/nm-device-wpan.h
new file mode 100644
index 00000000..969929ae
--- /dev/null
+++ b/src/core/devices/nm-device-wpan.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2018 Lubomir Rintel <lkundrak@v3.sk>
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_WPAN_H__
+#define __NETWORKMANAGER_DEVICE_WPAN_H__
+
+#define NM_TYPE_DEVICE_WPAN (nm_device_wpan_get_type())
+#define NM_DEVICE_WPAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpan))
+#define NM_DEVICE_WPAN_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WPAN, NMDeviceWpanClass))
+#define NM_IS_DEVICE_WPAN(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WPAN))
+#define NM_IS_DEVICE_WPAN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WPAN))
+#define NM_DEVICE_WPAN_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WPAN, NMDeviceWpanClass))
+
+typedef struct _NMDeviceWpan      NMDeviceWpan;
+typedef struct _NMDeviceWpanClass NMDeviceWpanClass;
+
+GType nm_device_wpan_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_WPAN_H__ */
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
new file mode 100644
index 00000000..040dd0b4
--- /dev/null
+++ b/src/core/devices/nm-device.c
@@ -0,0 +1,19022 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2018 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device.h"
+
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <netinet/if_ether.h>
+#include <linux/if.h>
+#include <linux/if_addr.h>
+#include <linux/rtnetlink.h>
+#include <linux/if_ether.h>
+#include <linux/if_infiniband.h>
+
+#include "nm-std-aux/unaligned.h"
+#include "nm-glib-aux/nm-dedup-multi.h"
+#include "nm-glib-aux/nm-random-utils.h"
+#include "systemd/nm-sd-utils-shared.h"
+
+#include "nm-base/nm-ethtool-base.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
+#include "nm-device-private.h"
+#include "nm-l3cfg.h"
+#include "nm-l3-config-data.h"
+#include "NetworkManagerUtils.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+#include "nm-platform/nm-platform-utils.h"
+#include "platform/nmp-object.h"
+#include "platform/nmp-rules-manager.h"
+#include "ndisc/nm-ndisc.h"
+#include "ndisc/nm-lndp-ndisc.h"
+#include "dhcp/nm-dhcp-manager.h"
+#include "dhcp/nm-dhcp-utils.h"
+#include "nm-act-request.h"
+#include "nm-proxy-config.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+#include "nm-pacrunner-manager.h"
+#include "dnsmasq/nm-dnsmasq-manager.h"
+#include "nm-dhcp-config.h"
+#include "nm-rfkill-manager.h"
+#include "nm-firewall-manager.h"
+#include "settings/nm-settings-connection.h"
+#include "settings/nm-settings.h"
+#include "nm-setting-ethtool.h"
+#include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-user.h"
+#include "nm-auth-utils.h"
+#include "nm-keep-alive.h"
+#include "nm-netns.h"
+#include "nm-dispatcher.h"
+#include "nm-config.h"
+#include "c-list/src/c-list.h"
+#include "dns/nm-dns-manager.h"
+#include "nm-acd-manager.h"
+#include "nm-core-internal.h"
+#include "systemd/nm-sd.h"
+#include "nm-lldp-listener.h"
+#include "nm-audit-manager.h"
+#include "nm-connectivity.h"
+#include "nm-dbus-interface.h"
+
+#include "nm-device-generic.h"
+#include "nm-device-vlan.h"
+#include "nm-device-vrf.h"
+#include "nm-device-wireguard.h"
+
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+#define DEFAULT_AUTOCONNECT TRUE
+
+static guint32
+dhcp_grace_period_from_timeout(guint32 timeout)
+{
+#define DHCP_GRACE_PERIOD_MULTIPLIER 2U
+
+    nm_assert(timeout > 0);
+    nm_assert(timeout < G_MAXINT32);
+
+    if (timeout < G_MAXUINT32 / DHCP_GRACE_PERIOD_MULTIPLIER)
+        return timeout * DHCP_GRACE_PERIOD_MULTIPLIER;
+
+    return G_MAXUINT32;
+}
+
+#define CARRIER_WAIT_TIME_MS           6000
+#define CARRIER_WAIT_TIME_AFTER_MTU_MS 10000
+
+#define NM_DEVICE_AUTH_RETRIES_UNSET    -1
+#define NM_DEVICE_AUTH_RETRIES_INFINITY -2
+#define NM_DEVICE_AUTH_RETRIES_DEFAULT  3
+
+/*****************************************************************************/
+
+typedef void (*ActivationHandleFunc)(NMDevice *self);
+
+typedef enum {
+    CLEANUP_TYPE_KEEP,
+    CLEANUP_TYPE_REMOVED,
+    CLEANUP_TYPE_DECONFIGURE,
+} CleanupType;
+
+typedef struct {
+    CList     lst_slave;
+    NMDevice *slave;
+    gulong    watch_id;
+    bool      slave_is_enslaved;
+    bool      configure;
+} SlaveInfo;
+
+typedef struct {
+    NMDevice *device;
+    guint     idle_add_id;
+    int       ifindex;
+} DeleteOnDeactivateData;
+
+typedef struct {
+    NMDevice *              device;
+    GCancellable *          cancellable;
+    NMPlatformAsyncCallback callback;
+    gpointer                callback_data;
+    guint                   num_vfs;
+    NMOptionBool            autoprobe;
+} SriovOp;
+
+typedef void (*AcdCallback)(NMDevice *, NMIP4Config **, gboolean);
+
+typedef enum {
+    /* The various NML3ConfigData types that we track explicitly. Note that
+     * their relative order matters: higher numbers in this enum means more
+     * important (and during merge overwrites other settings). */
+    L3_CONFIG_DATA_TYPE_LL_4,
+    L3_CONFIG_DATA_TYPE_AC_6,
+    L3_CONFIG_DATA_TYPE_DHCP_4,
+    L3_CONFIG_DATA_TYPE_DHCP_6,
+    L3_CONFIG_DATA_TYPE_DEV_4,
+    L3_CONFIG_DATA_TYPE_DEV_6,
+    L3_CONFIG_DATA_TYPE_SETTING,
+    _L3_CONFIG_DATA_TYPE_NUM,
+    _L3_CONFIG_DATA_TYPE_NONE,
+} L3ConfigDataType;
+
+typedef struct {
+    AcdCallback   callback;
+    NMDevice *    device;
+    NMIP4Config **configs;
+} AcdData;
+
+typedef enum {
+    HW_ADDR_TYPE_UNSET = 0,
+    HW_ADDR_TYPE_PERMANENT,
+    HW_ADDR_TYPE_EXPLICIT,
+    HW_ADDR_TYPE_GENERATED,
+} HwAddrType;
+
+typedef enum {
+    FIREWALL_STATE_UNMANAGED = 0,
+    FIREWALL_STATE_INITIALIZED,
+    FIREWALL_STATE_WAIT_STAGE_3,
+    FIREWALL_STATE_WAIT_IP_CONFIG,
+} FirewallState;
+
+typedef struct {
+    NMIPConfig *orig;    /* the original configuration applied to the device */
+    NMIPConfig *current; /* configuration after external changes.  NULL means
+                          * that the original configuration didn't change. */
+} AppliedConfig;
+
+typedef struct {
+    NMDhcpClient *client;
+    NMDhcpConfig *config;
+    gulong        state_sigid;
+    guint         grace_id;
+    bool          grace_pending : 1;
+    bool          was_active : 1;
+} DhcpData;
+
+struct _NMDeviceConnectivityHandle {
+    CList                        concheck_lst;
+    NMDevice *                   self;
+    NMDeviceConnectivityCallback callback;
+    gpointer                     user_data;
+    NMConnectivityCheckHandle *  c_handle;
+    guint64                      seq;
+    bool                         is_periodic : 1;
+    bool                         is_periodic_bump : 1;
+    bool                         is_periodic_bump_on_complete : 1;
+    int                          addr_family;
+};
+
+typedef struct {
+    int                     ifindex;
+    NMEthtoolFeatureStates *features;
+    NMOptionBool            requested[_NM_ETHTOOL_ID_FEATURE_NUM];
+    NMEthtoolCoalesceState *coalesce;
+    NMEthtoolRingState *    ring;
+} EthtoolState;
+
+typedef enum {
+    RESOLVER_WAIT_ADDRESS = 0,
+    RESOLVER_IN_PROGRESS,
+    RESOLVER_DONE,
+} ResolverState;
+
+typedef struct {
+    ResolverState state;
+    GResolver *   resolver;
+    GInetAddress *address;
+    GCancellable *cancellable;
+    char *        hostname;
+    NMDevice *    device;
+    guint         timeout_id; /* Used when waiting for the address */
+    int           addr_family;
+} HostnameResolver;
+
+/*****************************************************************************/
+
+enum {
+    STATE_CHANGED,
+    AUTOCONNECT_ALLOWED,
+    IP4_CONFIG_CHANGED,
+    IP6_CONFIG_CHANGED,
+    IP6_PREFIX_DELEGATED,
+    IP6_SUBNET_NEEDED,
+    REMOVED,
+    RECHECK_AUTO_ACTIVATE,
+    RECHECK_ASSUME,
+    DNS_LOOKUP_DONE,
+    LAST_SIGNAL,
+};
+static guint signals[LAST_SIGNAL] = {0};
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDevice,
+                             PROP_UDI,
+                             PROP_PATH,
+                             PROP_IFACE,
+                             PROP_IP_IFACE,
+                             PROP_DRIVER,
+                             PROP_DRIVER_VERSION,
+                             PROP_FIRMWARE_VERSION,
+                             PROP_CAPABILITIES,
+                             PROP_CARRIER,
+                             PROP_MTU,
+                             PROP_IP4_ADDRESS,
+                             PROP_IP4_CONFIG,
+                             PROP_DHCP4_CONFIG,
+                             PROP_IP6_CONFIG,
+                             PROP_DHCP6_CONFIG,
+                             PROP_STATE,
+                             PROP_STATE_REASON,
+                             PROP_ACTIVE_CONNECTION,
+                             PROP_DEVICE_TYPE,
+                             PROP_LINK_TYPE,
+                             PROP_MANAGED,
+                             PROP_AUTOCONNECT,
+                             PROP_FIRMWARE_MISSING,
+                             PROP_NM_PLUGIN_MISSING,
+                             PROP_TYPE_DESC,
+                             PROP_RFKILL_TYPE,
+                             PROP_IFINDEX,
+                             PROP_AVAILABLE_CONNECTIONS,
+                             PROP_PHYSICAL_PORT_ID,
+                             PROP_MASTER,
+                             PROP_PARENT,
+                             PROP_HW_ADDRESS,
+                             PROP_PERM_HW_ADDRESS,
+                             PROP_HAS_PENDING_ACTION,
+                             PROP_METERED,
+                             PROP_LLDP_NEIGHBORS,
+                             PROP_REAL,
+                             PROP_SLAVES,
+                             PROP_STATISTICS_REFRESH_RATE_MS,
+                             PROP_STATISTICS_TX_BYTES,
+                             PROP_STATISTICS_RX_BYTES,
+                             PROP_IP4_CONNECTIVITY,
+                             PROP_IP6_CONNECTIVITY,
+                             PROP_INTERFACE_FLAGS, );
+
+typedef struct _NMDevicePrivate {
+    bool in_state_changed;
+
+    guint device_link_changed_id;
+    guint device_ip_link_changed_id;
+
+    NMDeviceState       state;
+    NMDeviceStateReason state_reason;
+    struct {
+        guint id;
+
+        /* The @state/@reason is only valid, when @id is set. */
+        NMDeviceState       state;
+        NMDeviceStateReason reason;
+    } queued_state;
+
+    union {
+        struct {
+            guint queued_ip_config_id_6;
+            guint queued_ip_config_id_4;
+        };
+        guint queued_ip_config_id_x[2];
+    };
+
+    GSList *pending_actions;
+    GSList *dad6_failed_addrs;
+
+    NMDBusTrackObjPath parent_device;
+
+    char *udi;
+    char *path;
+
+    union {
+        const char *const iface;
+        char *            iface_;
+    };
+    union {
+        const char *const ip_iface;
+        char *            ip_iface_;
+    };
+
+    union {
+        const int ifindex;
+        int       ifindex_;
+    };
+    union {
+        const int ip_ifindex;
+        int       ip_ifindex_;
+    };
+
+    NMNetnsSharedIPHandle *shared_ip_handle;
+
+    int parent_ifindex;
+
+    int auth_retries;
+
+    union {
+        struct {
+            HostnameResolver *hostname_resolver_6;
+            HostnameResolver *hostname_resolver_4;
+        };
+        HostnameResolver *hostname_resolver_x[2];
+    };
+
+    union {
+        const guint8 hw_addr_len; /* read-only */
+        guint8       hw_addr_len_;
+    };
+
+    HwAddrType hw_addr_type : 5;
+
+    bool real : 1;
+
+    bool update_ip_config_completed_v4 : 1;
+    bool update_ip_config_completed_v6 : 1;
+
+    NMDeviceType         type;
+    char *               type_desc;
+    NMLinkType           link_type;
+    NMDeviceCapabilities capabilities;
+    char *               driver;
+    char *               driver_version;
+    char *               firmware_version;
+    RfKillType           rfkill_type;
+    bool                 firmware_missing : 1;
+    bool                 nm_plugin_missing : 1;
+    bool
+        hw_addr_perm_fake : 1; /* whether the permanent HW address could not be read and is a fake */
+
+    NMUtilsStableType current_stable_id_type : 3;
+
+    bool nm_owned : 1; /* whether the device is a device owned and created by NM */
+
+    bool  assume_state_guess_assume : 1;
+    char *assume_state_connection_uuid;
+
+    guint64 udi_id;
+
+    GHashTable *available_connections;
+    char *      hw_addr;
+    char *      hw_addr_perm;
+    char *      hw_addr_initial;
+    char *      physical_port_id;
+    guint       dev_id;
+
+    NMUnmanagedFlags unmanaged_mask;
+    NMUnmanagedFlags unmanaged_flags;
+    DeleteOnDeactivateData
+        *delete_on_deactivate_data; /* data for scheduled cleanup when deleting link (g_idle_add) */
+
+    GCancellable *deactivating_cancellable;
+
+    NMActRequest *     queued_act_request;
+    bool               queued_act_request_is_waiting_for_carrier : 1;
+    NMDBusTrackObjPath act_request;
+
+    union {
+        struct {
+            guint activation_source_id_6;
+            guint activation_source_id_4; /* for layer2 and IPv4. */
+        };
+        guint activation_source_id_x[2];
+    };
+
+    union {
+        struct {
+            ActivationHandleFunc activation_source_func_6;
+            ActivationHandleFunc activation_source_func_4; /* for layer2 and IPv4. */
+        };
+        ActivationHandleFunc activation_source_func_x[2];
+    };
+
+    guint recheck_assume_id;
+
+    struct {
+        guint               call_id;
+        NMDeviceStateReason available_reason;
+        NMDeviceStateReason unavailable_reason;
+    } recheck_available;
+
+    struct {
+        NMDispatcherCallId *call_id;
+        NMDeviceState       post_state;
+        NMDeviceStateReason post_state_reason;
+    } dispatcher;
+
+    /* Link stuff */
+    guint             link_connected_id;
+    guint             link_disconnected_id;
+    guint             carrier_defer_id;
+    guint             carrier_wait_id;
+    gulong            config_changed_id;
+    gulong            ifindex_changed_id;
+    guint32           mtu;
+    guint32           ip6_mtu;
+    guint32           mtu_initial;
+    guint32           ip6_mtu_initial;
+    NMDeviceMtuSource mtu_source;
+
+    guint32 v4_route_table;
+    guint32 v6_route_table;
+
+    /* when carrier goes away, we give a grace period of _get_carrier_wait_ms()
+     * until taking action.
+     *
+     * When changing MTU, the device might take longer then that. So, whenever
+     * NM changes the MTU it sets @carrier_wait_until_ms to CARRIER_WAIT_TIME_AFTER_MTU_MS
+     * in the future. This is used to extend the grace period in this particular case. */
+    gint64 carrier_wait_until_ms;
+
+    union {
+        const NMDeviceSysIfaceState sys_iface_state;
+        NMDeviceSysIfaceState       sys_iface_state_;
+    };
+
+    bool carrier : 1;
+    bool ignore_carrier : 1;
+
+    bool up : 1; /* IFF_UP */
+
+    bool v4_commit_first_time : 1;
+    bool v6_commit_first_time : 1;
+
+    bool default_route_metric_penalty_ip4_has : 1;
+    bool default_route_metric_penalty_ip6_has : 1;
+
+    bool v4_route_table_initialized : 1;
+    bool v6_route_table_initialized : 1;
+
+    bool l3config_merge_flags_has : 1;
+
+    bool v4_route_table_all_sync_before : 1;
+    bool v6_route_table_all_sync_before : 1;
+
+    NMDeviceAutoconnectBlockedFlags autoconnect_blocked_flags : 5;
+
+    bool is_enslaved : 1;
+
+    bool ipv6ll_handle : 1; /* TRUE if NM handles the device's IPv6LL address */
+    bool ipv6ll_has : 1;
+    bool ndisc_started : 1;
+    bool device_link_changed_down : 1;
+
+    bool concheck_rp_filter_checked : 1;
+
+    NMDeviceStageState stage1_sriov_state : 3;
+
+    /* Generic DHCP stuff */
+    char *dhcp_anycast_address;
+
+    char *current_stable_id;
+
+    /* Proxy Configuration */
+    NMProxyConfig *    proxy_config;
+    NMPacrunnerConfId *pacrunner_conf_id;
+
+    /* IP configuration info. Combined config from VPN, settings, and device */
+    union {
+        struct {
+            NMIP6Config *ip_config_6;
+            NMIP4Config *ip_config_4;
+        };
+        NMIPConfig *ip_config_x[2];
+    };
+
+    /* Config from DHCP, PPP, LLv4, etc */
+    AppliedConfig dev_ip_config_4;
+
+    /* config from the setting */
+    union {
+        struct {
+            NMIP6Config *con_ip_config_6;
+            NMIP4Config *con_ip_config_4;
+        };
+        NMIPConfig *con_ip_config_x[2];
+    };
+
+    /* Stuff added outside NM */
+    union {
+        struct {
+            NMIP6Config *ext_ip_config_6;
+            NMIP4Config *ext_ip_config_4;
+        };
+        NMIPConfig *ext_ip_config_x[2];
+    };
+
+    /* VPNs which use this device */
+    union {
+        struct {
+            GSList *vpn_configs_6;
+            GSList *vpn_configs_4;
+        };
+        GSList *vpn_configs_x[2];
+    };
+
+    /* Extra device configuration, injected by the subclass of NMDevice.
+     * This is used for example by NMDeviceModem for WWAN configuration. */
+    union {
+        struct {
+            AppliedConfig dev2_ip_config_6;
+            AppliedConfig dev2_ip_config_4;
+        };
+        AppliedConfig dev2_ip_config_x[2];
+    };
+
+    /* DHCPv4 tracking */
+    struct {
+        char *pac_url;
+    } dhcp4;
+
+    struct {
+        /* IP6 config from DHCP */
+        AppliedConfig ip6_config;
+        /* Event ID of the current IP6 config from DHCP */
+        char *           event_id;
+        gulong           prefix_sigid;
+        NMNDiscDHCPLevel mode;
+        guint            needed_prefixes;
+    } dhcp6;
+
+    union {
+        struct {
+            DhcpData dhcp_data_6;
+            DhcpData dhcp_data_4;
+        };
+        DhcpData dhcp_data_x[2];
+    };
+
+    struct {
+        NMLogDomain log_domain;
+        guint       timeout;
+        guint       watch;
+        GPid        pid;
+        char *      binary;
+        char *      address;
+        guint       deadline;
+    } gw_ping;
+
+    /* dnsmasq stuff for shared connections */
+    NMDnsMasqManager *dnsmasq_manager;
+    gulong            dnsmasq_state_id;
+
+    /* Firewall */
+    FirewallState            fw_state : 4;
+    NMFirewallManager *      fw_mgr;
+    NMFirewallManagerCallId *fw_call;
+
+    /* IPv4LL stuff */
+    sd_ipv4ll *ipv4ll;
+    guint      ipv4ll_timeout;
+    guint      rt6_temporary_not_available_id;
+
+    /* IPv4 DAD stuff */
+    struct {
+        GSList *      dad_list;
+        NMAcdManager *announcing;
+    } acd;
+
+    union {
+        struct {
+            const NMDeviceIPState ip_state_6;
+            const NMDeviceIPState ip_state_4;
+        };
+        union {
+            const NMDeviceIPState ip_state_x[2];
+            NMDeviceIPState       ip_state_x_[2];
+        };
+    };
+
+    AppliedConfig   ac_ip6_config;           /* config from IPv6 autoconfiguration */
+    NMIP6Config *   ext_ip6_config_captured; /* Configuration captured from platform. */
+    NMIP6Config *   dad6_ip6_config;
+    struct in6_addr ipv6ll_addr;
+
+    GHashTable *rt6_temporary_not_available;
+
+    NMNDisc *                 ndisc;
+    gulong                    ndisc_changed_id;
+    gulong                    ndisc_timeout_id;
+    NMSettingIP6ConfigPrivacy ndisc_use_tempaddr;
+
+    guint  linklocal6_timeout_id;
+    guint8 linklocal6_dad_counter;
+
+    GHashTable *ip6_saved_properties;
+
+    EthtoolState *ethtool_state;
+
+    gboolean needs_ip6_subnet;
+
+    /* master interface for bridge/bond/team slave */
+    NMDevice *master;
+    gulong    master_ready_id;
+    int       master_ifindex;
+
+    /* slave management */
+    CList slaves; /* list of SlaveInfo */
+
+    NMMetered metered;
+
+    NMSettings *settings;
+    NMManager * manager;
+
+    NMNetns *netns;
+
+    NMLldpListener *lldp_listener;
+
+    NMConnectivity *concheck_mgr;
+    CList           concheck_lst_head;
+    struct {
+        /* if periodic checks are enabled, this is the source id for the next check. */
+        guint p_cur_id;
+
+        /* the currently configured max periodic interval. */
+        guint p_max_interval;
+
+        /* the current interval. If we are probing, the interval might be lower
+         * then the configured max interval. */
+        guint p_cur_interval;
+
+        /* the timestamp, when we last scheduled the timer p_cur_id with current interval
+         * p_cur_interval. */
+        gint64 p_cur_basetime_ns;
+
+        NMConnectivityState state;
+    } concheck_x[2];
+
+    guint   check_delete_unrealized_id;
+    guint32 interface_flags;
+
+    struct {
+        SriovOp *pending; /* SR-IOV operation currently running */
+        SriovOp *next;    /* next SR-IOV operation scheduled */
+    } sriov;
+    guint sriov_reset_pending;
+
+    struct {
+        guint   timeout_id;
+        guint   refresh_rate_ms;
+        guint64 tx_bytes;
+        guint64 rx_bytes;
+    } stats;
+
+    bool mtu_force_set_done : 1;
+} NMDevicePrivate;
+
+G_DEFINE_ABSTRACT_TYPE(NMDevice, nm_device, NM_TYPE_DBUS_OBJECT)
+
+#define NM_DEVICE_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMDevice, NM_IS_DEVICE)
+
+/*****************************************************************************/
+
+static const NMDBusInterfaceInfoExtended interface_info_device;
+static const GDBusSignalInfo             signal_info_state_changed;
+
+static void nm_device_set_proxy_config(NMDevice *self, const char *pac_url);
+
+static gboolean update_ext_ip_config(NMDevice *self, int addr_family, gboolean intersect_configs);
+
+static gboolean nm_device_set_ip_config(NMDevice *  self,
+                                        int         addr_family,
+                                        NMIPConfig *config,
+                                        gboolean    commit,
+                                        GPtrArray * ip4_dev_route_blacklist);
+
+static gboolean ip_config_merge_and_apply(NMDevice *self, int addr_family, gboolean commit);
+
+static gboolean nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure);
+static void     nm_device_slave_notify_enslave(NMDevice *self, gboolean success);
+static void     nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason);
+
+static void     addrconf6_start_with_link_ready(NMDevice *self);
+static gboolean linklocal6_start(NMDevice *self);
+
+static guint32 default_route_metric_penalty_get(NMDevice *self, int addr_family);
+
+static guint _prop_get_ipv4_dad_timeout(NMDevice *self);
+
+static NMIP6Config *dad6_get_pending_addresses(NMDevice *self);
+
+static void   _carrier_wait_check_queued_act_request(NMDevice *self);
+static gint64 _get_carrier_wait_ms(NMDevice *self);
+
+static const char *_activation_func_to_string(ActivationHandleFunc func);
+
+static void
+_set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason, gboolean quitting);
+static void             queued_state_clear(NMDevice *device);
+static gboolean         queued_ip4_config_change(gpointer user_data);
+static gboolean         queued_ip6_config_change(gpointer user_data);
+static void             ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data);
+static gboolean         ip_config_valid(NMDeviceState state);
+static NMActStageReturn dhcp4_start(NMDevice *self);
+static gboolean         dhcp6_start(NMDevice *self, gboolean wait_for_ll);
+static void             nm_device_start_ip_check(NMDevice *self);
+static void             realize_start_setup(NMDevice *            self,
+                                            const NMPlatformLink *plink,
+                                            gboolean              assume_state_guess_assume,
+                                            const char *          assume_state_connection_uuid,
+                                            gboolean              set_nm_owned,
+                                            NMUnmanFlagOp         unmanaged_user_explicit,
+                                            gboolean              force_platform_init);
+static void             _set_mtu(NMDevice *self, guint32 mtu);
+static void             _commit_mtu(NMDevice *self, const NMIP4Config *config);
+static void             _cancel_activation(NMDevice *self);
+
+static void concheck_update_state(NMDevice *          self,
+                                  int                 addr_family,
+                                  NMConnectivityState state,
+                                  gboolean            is_periodic);
+
+static void sriov_op_cb(GError *error, gpointer user_data);
+
+static void device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self);
+static gboolean device_link_changed(NMDevice *self);
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    queued_state_to_string,
+    NMDeviceState,
+    NM_UTILS_LOOKUP_DEFAULT(NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "???"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNKNOWN,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unknown"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNMANAGED,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unmanaged"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_UNAVAILABLE,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "unavailable"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_DISCONNECTED,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "disconnected"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_PREPARE,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "prepare"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_CONFIG,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "config"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_NEED_AUTH,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "need-auth"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_IP_CONFIG,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "ip-config"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_IP_CHECK,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "ip-check"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_SECONDARIES,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "secondaries"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_ACTIVATED,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "activated"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_DEACTIVATING,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "deactivating"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_FAILED,
+                             NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "failed"), );
+
+const char *
+nm_device_state_to_str(NMDeviceState state)
+{
+    return queued_state_to_string(state) + NM_STRLEN(NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE);
+}
+
+NM_UTILS_LOOKUP_STR_DEFINE(
+    nm_device_state_reason_to_str,
+    NMDeviceStateReason,
+    NM_UTILS_LOOKUP_DEFAULT(NULL),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNKNOWN, "unknown"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NONE, "none"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NOW_MANAGED, "managed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NOW_UNMANAGED, "unmanaged"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONFIG_FAILED, "config-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE, "ip-config-unavailable"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED, "ip-config-expired"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NO_SECRETS, "no-secrets"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, "supplicant-disconnect"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED,
+                             "supplicant-config-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED, "supplicant-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT, "supplicant-timeout"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_START_FAILED, "ppp-start-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_DISCONNECT, "ppp-disconnect"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PPP_FAILED, "ppp-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_START_FAILED, "dhcp-start-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_ERROR, "dhcp-error"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DHCP_FAILED, "dhcp-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SHARED_START_FAILED, "sharing-start-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SHARED_FAILED, "sharing-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED, "autoip-start-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_ERROR, "autoip-error"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_AUTOIP_FAILED, "autoip-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_BUSY, "modem-busy"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE, "modem-no-dialtone"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER, "modem-no-carrier"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT, "modem-dial-timeout"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED, "modem-dial-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED, "modem-init-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_APN_FAILED, "gsm-apn-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING,
+                             "gsm-registration-idle"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED,
+                             "gsm-registration-denied"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT,
+                             "gsm-registration-timeout"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED,
+                             "gsm-registration-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED, "gsm-pin-check-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_FIRMWARE_MISSING, "firmware-missing"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_REMOVED, "removed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SLEEPING, "sleeping"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_REMOVED, "connection-removed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_USER_REQUESTED, "user-requested"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CARRIER, "carrier-changed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED, "connection-assumed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE, "supplicant-available"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND, "modem-not-found"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_BT_FAILED, "bluetooth-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED, "gsm-sim-not-inserted"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED, "gsm-sim-pin-required"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED, "gsm-sim-puk-required"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_GSM_SIM_WRONG, "gsm-sim-wrong"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_INFINIBAND_MODE, "infiniband-mode"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED, "dependency-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_BR2684_FAILED, "br2684-bridge-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_MANAGER_UNAVAILABLE,
+                             "modem-manager-unavailable"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SSID_NOT_FOUND, "ssid-not-found"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SECONDARY_CONNECTION_FAILED,
+                             "secondary-connection-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_DCB_FCOE_FAILED, "dcb-fcoe-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED, "teamd-control-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_FAILED, "modem-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_AVAILABLE, "modem-available"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT, "sim-pin-incorrect"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NEW_ACTIVATION, "new-activation"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PARENT_CHANGED, "parent-changed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_PARENT_MANAGED_CHANGED,
+                             "parent-managed-changed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_OVSDB_FAILED, "ovsdb-failed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE, "ip-address-duplicate"),
+    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"), );
+
+#define reason_to_string_a(reason) NM_UTILS_LOOKUP_STR_A(nm_device_state_reason_to_str, reason)
+
+static NM_UTILS_LOOKUP_STR_DEFINE(mtu_source_to_str,
+                                  NMDeviceMtuSource,
+                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_NONE, "none"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_PARENT, "parent"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_IP_CONFIG,
+                                                           "ip-config"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_MTU_SOURCE_CONNECTION,
+                                                           "connection"), );
+
+/*****************************************************************************/
+
+static void
+_hostname_resolver_free(HostnameResolver *resolver)
+{
+    if (!resolver)
+        return;
+
+    nm_clear_g_source(&resolver->timeout_id);
+    nm_clear_g_cancellable(&resolver->cancellable);
+    nm_g_object_unref(resolver->resolver);
+    nm_g_object_unref(resolver->address);
+    g_free(resolver->hostname);
+    nm_g_slice_free(resolver);
+}
+
+/*****************************************************************************/
+
+static NMSettingIP6ConfigPrivacy
+_ip6_privacy_clamp(NMSettingIP6ConfigPrivacy use_tempaddr)
+{
+    switch (use_tempaddr) {
+    case NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED:
+    case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR:
+    case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR:
+        return use_tempaddr;
+    default:
+        return NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
+    }
+}
+
+/*****************************************************************************/
+
+static const char *
+_prop_get_connection_stable_id(NMDevice *         self,
+                               NMConnection *     connection,
+                               NMUtilsStableType *out_stable_type)
+{
+    NMDevicePrivate *priv;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_CONNECTION(connection));
+    nm_assert(out_stable_type);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* we cache the generated stable ID for the time of an activation.
+     *
+     * The reason is, that we don't want the stable-id to change as long
+     * as the device is active.
+     *
+     * Especially with ${RANDOM} stable-id we want to generate *one* configuration
+     * for each activation. */
+    if (G_UNLIKELY(!priv->current_stable_id)) {
+        gs_free char *       default_id = NULL;
+        gs_free char *       generated  = NULL;
+        NMUtilsStableType    stable_type;
+        NMSettingConnection *s_con;
+        gboolean             hwaddr_is_fake;
+        const char *         hwaddr;
+        const char *         stable_id;
+        const char *         uuid;
+
+        s_con = nm_connection_get_setting_connection(connection);
+
+        stable_id = nm_setting_connection_get_stable_id(s_con);
+
+        if (!stable_id) {
+            default_id =
+                nm_config_data_get_connection_default(NM_CONFIG_GET_DATA,
+                                                      NM_CON_DEFAULT("connection.stable-id"),
+                                                      self);
+            stable_id = default_id;
+        }
+
+        uuid = nm_connection_get_uuid(connection);
+
+        /* the cloned-mac-address may be generated based on the stable-id.
+         * Thus, at this point, we can only use the permanent MAC address
+         * as seed. */
+        hwaddr = nm_device_get_permanent_hw_address_full(self, TRUE, &hwaddr_is_fake);
+
+        stable_type = nm_utils_stable_id_parse(stable_id,
+                                               nm_device_get_ip_iface(self),
+                                               !hwaddr_is_fake ? hwaddr : NULL,
+                                               nm_utils_boot_id_str(),
+                                               uuid,
+                                               &generated);
+
+        /* current_stable_id_type is a bitfield! */
+        priv->current_stable_id_type = stable_type;
+        nm_assert(stable_type <= (NMUtilsStableType) 0x3);
+        nm_assert(stable_type + (NMUtilsStableType) 1 > (NMUtilsStableType) 0);
+        nm_assert(priv->current_stable_id_type == stable_type);
+
+        if (stable_type == NM_UTILS_STABLE_TYPE_UUID)
+            priv->current_stable_id = g_strdup(uuid);
+        else if (stable_type == NM_UTILS_STABLE_TYPE_STABLE_ID)
+            priv->current_stable_id = g_strdup(stable_id);
+        else if (stable_type == NM_UTILS_STABLE_TYPE_GENERATED)
+            priv->current_stable_id =
+                nm_str_realloc(nm_utils_stable_id_generated_complete(generated));
+        else {
+            nm_assert(stable_type == NM_UTILS_STABLE_TYPE_RANDOM);
+            priv->current_stable_id = nm_str_realloc(nm_utils_stable_id_random());
+        }
+        _LOGT(LOGD_DEVICE,
+              "stable-id: type=%d, \"%s\""
+              "%s%s%s",
+              (int) priv->current_stable_id_type,
+              priv->current_stable_id,
+              NM_PRINT_FMT_QUOTED(stable_type == NM_UTILS_STABLE_TYPE_GENERATED,
+                                  " from \"",
+                                  generated,
+                                  "\"",
+                                  ""));
+    }
+
+    nm_assert(priv->current_stable_id);
+    *out_stable_type = priv->current_stable_id_type;
+    return priv->current_stable_id;
+}
+
+static GBytes *
+_prop_get_ipv6_dhcp_duid(NMDevice *    self,
+                         NMConnection *connection,
+                         GBytes *      hwaddr,
+                         gboolean *    out_enforce)
+{
+    NMSettingIPConfig *s_ip6;
+    const char *       duid;
+    gs_free char *     duid_default = NULL;
+    const char *       duid_error;
+    GBytes *           duid_out;
+    gboolean           duid_enforce = TRUE;
+    gs_free char *     logstr1      = NULL;
+    const guint8 *     hwaddr_bin;
+    gsize              hwaddr_len;
+    int                arp_type;
+
+    s_ip6 = nm_connection_get_setting_ip6_config(connection);
+    duid  = nm_setting_ip6_config_get_dhcp_duid(NM_SETTING_IP6_CONFIG(s_ip6));
+
+    if (!duid) {
+        duid_default = nm_config_data_get_connection_default(NM_CONFIG_GET_DATA,
+                                                             NM_CON_DEFAULT("ipv6.dhcp-duid"),
+                                                             self);
+        duid         = duid_default;
+        if (!duid)
+            duid = "lease";
+    }
+
+    if (nm_streq(duid, "lease")) {
+        duid_enforce = FALSE;
+        duid_out     = nm_utils_generate_duid_from_machine_id();
+        goto out_good;
+    }
+
+    if (!_nm_utils_dhcp_duid_valid(duid, &duid_out)) {
+        duid_error = "invalid duid";
+        goto out_fail;
+    }
+
+    if (duid_out)
+        goto out_good;
+
+    if (NM_IN_STRSET(duid, "ll", "llt")) {
+        if (!hwaddr) {
+            duid_error = "missing link-layer address";
+            goto out_fail;
+        }
+
+        hwaddr_bin = g_bytes_get_data(hwaddr, &hwaddr_len);
+        arp_type   = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len);
+        if (arp_type < 0) {
+            duid_error = "unsupported link-layer address";
+            goto out_fail;
+        }
+
+        if (nm_streq(duid, "ll"))
+            duid_out = nm_utils_generate_duid_ll(arp_type, hwaddr_bin, hwaddr_len);
+        else {
+            duid_out = nm_utils_generate_duid_llt(arp_type,
+                                                  hwaddr_bin,
+                                                  hwaddr_len,
+                                                  nm_utils_host_id_get_timestamp_ns()
+                                                      / NM_UTILS_NSEC_PER_SEC);
+        }
+
+        goto out_good;
+    }
+
+    if (NM_IN_STRSET(duid, "stable-ll", "stable-llt", "stable-uuid")) {
+        /* preferably, we would salt the checksum differently for each @duid type. We missed
+         * to do that initially, so most types use the DEFAULT_SALT.
+         *
+         * Implementations that are added later, should use a distinct salt instead,
+         * like "stable-ll"/"stable-llt" with ARPHRD_INFINIBAND below. */
+        const guint32         DEFAULT_SALT   = 670531087u;
+        nm_auto_free_checksum GChecksum *sum = NULL;
+        NMUtilsStableType                stable_type;
+        const char *                     stable_id = NULL;
+        guint32                          salted_header;
+        const guint8 *                   host_id;
+        gsize                            host_id_len;
+        union {
+            guint8 sha256[NM_UTILS_CHECKSUM_LENGTH_SHA256];
+            guint8 hwaddr_eth[ETH_ALEN];
+            guint8 hwaddr_infiniband[INFINIBAND_ALEN];
+            NMUuid uuid;
+            struct _nm_packed {
+                guint8  hwaddr[ETH_ALEN];
+                guint32 timestamp;
+            } llt_eth;
+            struct _nm_packed {
+                guint8  hwaddr[INFINIBAND_ALEN];
+                guint32 timestamp;
+            } llt_infiniband;
+        } digest;
+
+        stable_id = _prop_get_connection_stable_id(self, connection, &stable_type);
+
+        if (NM_IN_STRSET(duid, "stable-ll", "stable-llt")) {
+            /* for stable LL/LLT DUIDs, we still need a hardware address to detect
+             * the arp-type. Alternatively, we would be able to detect it based on
+             * other means (e.g. NMDevice type), but instead require the hardware
+             * address to be present. This is at least consistent with the "ll"/"llt"
+             * modes above. */
+            if (!hwaddr) {
+                duid_error = "missing link-layer address";
+                goto out_fail;
+            }
+            if ((arp_type = nm_utils_arp_type_detect_from_hwaddrlen(g_bytes_get_size(hwaddr)))
+                < 0) {
+                duid_error = "unsupported link-layer address";
+                goto out_fail;
+            }
+
+            if (arp_type == ARPHRD_ETHER)
+                salted_header = DEFAULT_SALT;
+            else {
+                nm_assert(arp_type == ARPHRD_INFINIBAND);
+                salted_header = 0x42492CEFu + ((guint32) arp_type);
+            }
+        } else {
+            salted_header = DEFAULT_SALT;
+            arp_type      = -1;
+        }
+
+        salted_header = htonl(salted_header + ((guint32) stable_type));
+
+        nm_utils_host_id_get(&host_id, &host_id_len);
+
+        sum = g_checksum_new(G_CHECKSUM_SHA256);
+        g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header));
+        g_checksum_update(sum, (const guchar *) stable_id, -1);
+        g_checksum_update(sum, (const guchar *) host_id, host_id_len);
+        nm_utils_checksum_get_digest(sum, digest.sha256);
+
+        G_STATIC_ASSERT_EXPR(sizeof(digest) == sizeof(digest.sha256));
+
+        if (nm_streq(duid, "stable-ll")) {
+            switch (arp_type) {
+            case ARPHRD_ETHER:
+                duid_out = nm_utils_generate_duid_ll(arp_type,
+                                                     digest.hwaddr_eth,
+                                                     sizeof(digest.hwaddr_eth));
+                break;
+            case ARPHRD_INFINIBAND:
+                duid_out = nm_utils_generate_duid_ll(arp_type,
+                                                     digest.hwaddr_infiniband,
+                                                     sizeof(digest.hwaddr_infiniband));
+                break;
+            default:
+                g_return_val_if_reached(NULL);
+            }
+        } else if (nm_streq(duid, "stable-llt")) {
+            gint64  time;
+            guint32 timestamp;
+
+#define EPOCH_DATETIME_THREE_YEARS (356 * 24 * 3600 * 3)
+
+            /* We want a variable time between the host_id timestamp and three years
+             * before. Let's compute the time (in seconds) from 0 to 3 years; then we'll
+             * subtract it from the host_id timestamp.
+             */
+            time = nm_utils_host_id_get_timestamp_ns() / NM_UTILS_NSEC_PER_SEC;
+
+            /* don't use too old timestamps. They cannot be expressed in DUID-LLT and
+             * would all be truncated to zero. */
+            time = NM_MAX(time, NM_UTILS_EPOCH_DATETIME_200001010000 + EPOCH_DATETIME_THREE_YEARS);
+
+            switch (arp_type) {
+            case ARPHRD_ETHER:
+                timestamp = unaligned_read_be32(&digest.llt_eth.timestamp);
+                time -= timestamp % EPOCH_DATETIME_THREE_YEARS;
+                duid_out = nm_utils_generate_duid_llt(arp_type,
+                                                      digest.llt_eth.hwaddr,
+                                                      sizeof(digest.llt_eth.hwaddr),
+                                                      time);
+                break;
+            case ARPHRD_INFINIBAND:
+                timestamp = unaligned_read_be32(&digest.llt_infiniband.timestamp);
+                time -= timestamp % EPOCH_DATETIME_THREE_YEARS;
+                duid_out = nm_utils_generate_duid_llt(arp_type,
+                                                      digest.llt_infiniband.hwaddr,
+                                                      sizeof(digest.llt_infiniband.hwaddr),
+                                                      time);
+                break;
+            default:
+                g_return_val_if_reached(NULL);
+            }
+        } else {
+            nm_assert(nm_streq(duid, "stable-uuid"));
+            duid_out = nm_utils_generate_duid_uuid(&digest.uuid);
+        }
+
+        goto out_good;
+    }
+
+    g_return_val_if_reached(NULL);
+
+out_fail:
+    nm_assert(!duid_out && duid_error);
+    {
+        NMUuid uuid;
+
+        _LOGW(LOGD_IP6 | LOGD_DHCP6,
+              "ipv6.dhcp-duid: failure to generate %s DUID: %s. Fallback to random DUID-UUID.",
+              duid,
+              duid_error);
+
+        nm_utils_random_bytes(&uuid, sizeof(uuid));
+        duid_out = nm_utils_generate_duid_uuid(&uuid);
+    }
+
+out_good:
+    nm_assert(duid_out);
+    _LOGD(LOGD_IP6 | LOGD_DHCP6,
+          "ipv6.dhcp-duid: generate %s DUID '%s' (%s)",
+          duid,
+          (logstr1 = nm_dhcp_utils_duid_to_string(duid_out)),
+          duid_enforce ? "enforcing" : "prefer lease");
+
+    NM_SET_OUT(out_enforce, duid_enforce);
+    return duid_out;
+}
+
+static guint32
+_prop_get_ipv6_ra_timeout(NMDevice *self)
+{
+    NMConnection *connection;
+    gint32        timeout;
+
+    G_STATIC_ASSERT_EXPR(NM_RA_TIMEOUT_DEFAULT == 0);
+    G_STATIC_ASSERT_EXPR(NM_RA_TIMEOUT_INFINITY == G_MAXINT32);
+
+    connection = nm_device_get_applied_connection(self);
+
+    timeout = nm_setting_ip6_config_get_ra_timeout(
+        NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection)));
+    if (timeout > 0)
+        return timeout;
+    nm_assert(timeout == 0);
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("ipv6.ra-timeout"),
+                                                       self,
+                                                       0,
+                                                       G_MAXINT32,
+                                                       0);
+}
+
+static NMSettingConnectionMdns
+_prop_get_connection_mdns(NMDevice *self)
+{
+    NMConnection *          connection;
+    NMSettingConnectionMdns mdns = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_MDNS_DEFAULT);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        mdns = nm_setting_connection_get_mdns(nm_connection_get_setting_connection(connection));
+    if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+        return mdns;
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("connection.mdns"),
+                                                       self,
+                                                       NM_SETTING_CONNECTION_MDNS_NO,
+                                                       NM_SETTING_CONNECTION_MDNS_YES,
+                                                       NM_SETTING_CONNECTION_MDNS_DEFAULT);
+}
+
+static NMSettingConnectionLlmnr
+_prop_get_connection_llmnr(NMDevice *self)
+{
+    NMConnection *           connection;
+    NMSettingConnectionLlmnr llmnr = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_LLMNR_DEFAULT);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        llmnr = nm_setting_connection_get_llmnr(nm_connection_get_setting_connection(connection));
+    if (llmnr != NM_SETTING_CONNECTION_LLMNR_DEFAULT)
+        return llmnr;
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("connection.llmnr"),
+                                                       self,
+                                                       NM_SETTING_CONNECTION_LLMNR_NO,
+                                                       NM_SETTING_CONNECTION_LLMNR_YES,
+                                                       NM_SETTING_CONNECTION_LLMNR_DEFAULT);
+}
+
+static guint32
+_prop_get_ipvx_route_table(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceClass *      klass;
+    NMConnection *       connection;
+    NMSettingIPConfig *  s_ip;
+    guint32              route_table    = 0;
+    gboolean             is_user_config = TRUE;
+    NMSettingConnection *s_con;
+    NMSettingVrf *       s_vrf;
+
+    nm_assert_addr_family(addr_family);
+
+    /* the route table setting affects how we sync routes. We shall
+     * not change it while the device is active, hence, cache it. */
+    if (NM_IS_IPv4(addr_family)) {
+        if (priv->v4_route_table_initialized)
+            return priv->v4_route_table;
+    } else {
+        if (priv->v6_route_table_initialized)
+            return priv->v6_route_table;
+    }
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+        if (s_ip)
+            route_table = nm_setting_ip_config_get_route_table(s_ip);
+    }
+    if (route_table == 0u) {
+        gint64 v;
+
+        v = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                        NM_IS_IPv4(addr_family)
+                                                            ? NM_CON_DEFAULT("ipv4.route-table")
+                                                            : NM_CON_DEFAULT("ipv6.route-table"),
+                                                        self,
+                                                        0,
+                                                        G_MAXUINT32,
+                                                        -1);
+        if (v != -1) {
+            route_table    = v;
+            is_user_config = FALSE;
+        }
+    }
+
+    if (route_table == 0u && connection
+        && (s_con = nm_connection_get_setting_connection(connection))
+        && (nm_streq0(nm_setting_connection_get_slave_type(s_con), NM_SETTING_VRF_SETTING_NAME)
+            && priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF)) {
+        const NMPlatformLnkVrf *lnk;
+
+        lnk = nm_platform_link_get_lnk_vrf(nm_device_get_platform(self),
+                                           nm_device_get_ifindex(priv->master),
+                                           NULL);
+
+        if (lnk)
+            route_table = lnk->table;
+    }
+
+    if (route_table == 0u && connection
+        && (s_vrf = (NMSettingVrf *) nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF))) {
+        route_table = nm_setting_vrf_get_table(s_vrf);
+    }
+
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (klass->coerce_route_table)
+        route_table = klass->coerce_route_table(self, addr_family, route_table, is_user_config);
+
+    if (NM_IS_IPv4(addr_family)) {
+        priv->v4_route_table_initialized = TRUE;
+        priv->v4_route_table             = route_table;
+    } else {
+        priv->v6_route_table_initialized = TRUE;
+        priv->v6_route_table             = route_table;
+    }
+
+    _LOGT(LOGD_DEVICE,
+          "ipv%c.route-table = %u%s",
+          nm_utils_addr_family_to_char(addr_family),
+          (guint)(route_table ?: RT_TABLE_MAIN),
+          route_table != 0u ? "" : " (policy routing not enabled)");
+
+    return route_table;
+}
+
+static gboolean
+_prop_get_connection_lldp(NMDevice *self)
+{
+    NMConnection *          connection;
+    NMSettingConnection *   s_con;
+    NMSettingConnectionLldp lldp = NM_SETTING_CONNECTION_LLDP_DEFAULT;
+
+    connection = nm_device_get_applied_connection(self);
+    g_return_val_if_fail(connection, FALSE);
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_return_val_if_fail(s_con, FALSE);
+
+    lldp = nm_setting_connection_get_lldp(s_con);
+    if (lldp == NM_SETTING_CONNECTION_LLDP_DEFAULT) {
+        lldp = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                           NM_CON_DEFAULT("connection.lldp"),
+                                                           self,
+                                                           NM_SETTING_CONNECTION_LLDP_DEFAULT,
+                                                           NM_SETTING_CONNECTION_LLDP_ENABLE_RX,
+                                                           NM_SETTING_CONNECTION_LLDP_DEFAULT);
+        if (lldp == NM_SETTING_CONNECTION_LLDP_DEFAULT)
+            lldp = NM_SETTING_CONNECTION_LLDP_DISABLE;
+    }
+    return lldp == NM_SETTING_CONNECTION_LLDP_ENABLE_RX;
+}
+
+static guint
+_prop_get_ipv4_dad_timeout(NMDevice *self)
+{
+    NMConnection *     connection;
+    NMSettingIPConfig *s_ip4   = NULL;
+    int                timeout = -1;
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        s_ip4 = nm_connection_get_setting_ip4_config(connection);
+    if (s_ip4)
+        timeout = nm_setting_ip_config_get_dad_timeout(s_ip4);
+    if (timeout >= 0)
+        return timeout;
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("ipv4.dad-timeout"),
+                                                       self,
+                                                       0,
+                                                       NM_SETTING_IP_CONFIG_DAD_TIMEOUT_MAX,
+                                                       0);
+}
+
+static guint32
+_prop_get_ipvx_dhcp_timeout(NMDevice *self, int addr_family)
+{
+    NMDeviceClass *klass;
+    NMConnection * connection;
+    int            timeout_i;
+    guint32        timeout;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert_addr_family(addr_family);
+
+    connection = nm_device_get_applied_connection(self);
+
+    timeout_i = nm_setting_ip_config_get_dhcp_timeout(
+        nm_connection_get_setting_ip_config(connection, addr_family));
+    nm_assert(timeout_i >= 0 && timeout_i <= G_MAXINT32);
+
+    timeout = (guint32) timeout_i;
+    if (timeout)
+        goto out;
+
+    timeout = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                          NM_IS_IPv4(addr_family)
+                                                              ? NM_CON_DEFAULT("ipv4.dhcp-timeout")
+                                                              : NM_CON_DEFAULT("ipv6.dhcp-timeout"),
+                                                          self,
+                                                          0,
+                                                          G_MAXINT32,
+                                                          0);
+    if (timeout)
+        goto out;
+
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (klass->get_dhcp_timeout_for_device) {
+        timeout = klass->get_dhcp_timeout_for_device(self, addr_family);
+        if (timeout)
+            goto out;
+    }
+
+    timeout = NM_DHCP_TIMEOUT_DEFAULT;
+
+out:
+    G_STATIC_ASSERT_EXPR(G_MAXINT32 == NM_DHCP_TIMEOUT_INFINITY);
+    nm_assert(timeout > 0);
+    nm_assert(timeout <= G_MAXINT32);
+    return timeout;
+}
+
+/**
+ * _prop_get_ipvx_dhcp_iaid:
+ * @self: the #NMDevice
+ * @addr_family: the address family
+ * @connection: the connection
+ * @log_silent: whether to log the result.
+ * @out_is_explicit: on return, %TRUE if the user set a valid IAID in
+ *   the connection or in global configuration; %FALSE if the connection
+ *   property was empty and no valid global configuration was provided.
+ *
+ * Returns: a IAID value for this device and the given connection.
+ */
+static guint32
+_prop_get_ipvx_dhcp_iaid(NMDevice *    self,
+                         int           addr_family,
+                         NMConnection *connection,
+                         gboolean      log_silent,
+                         gboolean *    out_is_explicit)
+{
+    const int          IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMSettingIPConfig *s_ip;
+    const char *       iaid_str;
+    gs_free char *     iaid_str_free = NULL;
+    guint32            iaid;
+    const char *       iface;
+    const char *       fail_reason;
+    gboolean           is_explicit = TRUE;
+
+    s_ip     = nm_connection_get_setting_ip_config(connection, addr_family);
+    iaid_str = nm_setting_ip_config_get_dhcp_iaid(s_ip);
+    if (!iaid_str) {
+        iaid_str_free = nm_config_data_get_connection_default(
+            NM_CONFIG_GET_DATA,
+            IS_IPv4 ? NM_CON_DEFAULT("ipv4.dhcp-iaid") : NM_CON_DEFAULT("ipv6.dhcp-iaid"),
+            self);
+        iaid_str = iaid_str_free;
+        if (!iaid_str) {
+            iaid_str    = NM_IAID_IFNAME;
+            is_explicit = FALSE;
+        } else if (!_nm_utils_iaid_verify(iaid_str, NULL)) {
+            if (!log_silent) {
+                _LOGW(LOGD_DEVICE,
+                      "invalid global default '%s' for ipv%c.dhcp-iaid",
+                      iaid_str,
+                      nm_utils_addr_family_to_char(addr_family));
+            }
+            iaid_str    = NM_IAID_IFNAME;
+            is_explicit = FALSE;
+        }
+    }
+
+    if (nm_streq0(iaid_str, NM_IAID_MAC)) {
+        const NMPlatformLink *pllink;
+
+        pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self));
+        if (!pllink || pllink->l_address.len < 4) {
+            fail_reason = "invalid link-layer address";
+            goto out_fail;
+        }
+
+        /* @iaid is in native endianness. Use unaligned_read_be32()
+         * so that the IAID for a given MAC address is the same on
+         * BE and LE machines. */
+        iaid = unaligned_read_be32(&pllink->l_address.data[pllink->l_address.len - 4]);
+        goto out_good;
+    } else if (nm_streq0(iaid_str, NM_IAID_PERM_MAC)) {
+        guint8      hwaddr_buf[NM_UTILS_HWADDR_LEN_MAX];
+        const char *hwaddr_str;
+        gsize       hwaddr_len;
+
+        hwaddr_str = nm_device_get_permanent_hw_address(self);
+        if (!hwaddr_str) {
+            fail_reason = "no permanent link-layer address";
+            goto out_fail;
+        }
+
+        if (!_nm_utils_hwaddr_aton(hwaddr_str, hwaddr_buf, sizeof(hwaddr_buf), &hwaddr_len))
+            g_return_val_if_reached(0);
+
+        if (hwaddr_len < 4) {
+            fail_reason = "invalid link-layer address";
+            goto out_fail;
+        }
+
+        iaid = unaligned_read_be32(&hwaddr_buf[hwaddr_len - 4]);
+        goto out_good;
+    } else if (nm_streq(iaid_str, "stable")) {
+        nm_auto_free_checksum GChecksum *sum = NULL;
+        guint8                           digest[NM_UTILS_CHECKSUM_LENGTH_SHA1];
+        NMUtilsStableType                stable_type;
+        const char *                     stable_id;
+        guint32                          salted_header;
+        const guint8 *                   host_id;
+        gsize                            host_id_len;
+
+        stable_id     = _prop_get_connection_stable_id(self, connection, &stable_type);
+        salted_header = htonl(53390459 + stable_type);
+        nm_utils_host_id_get(&host_id, &host_id_len);
+        iface = nm_device_get_ip_iface(self);
+
+        sum = g_checksum_new(G_CHECKSUM_SHA1);
+        g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header));
+        g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1);
+        g_checksum_update(sum, (const guchar *) iface, strlen(iface) + 1);
+        g_checksum_update(sum, (const guchar *) host_id, host_id_len);
+        nm_utils_checksum_get_digest(sum, digest);
+
+        iaid = unaligned_read_be32(digest);
+        goto out_good;
+    } else if ((iaid = _nm_utils_ascii_str_to_int64(iaid_str, 10, 0, G_MAXUINT32, -1)) != -1) {
+        goto out_good;
+    } else {
+        iface = nm_device_get_ip_iface(self);
+        iaid  = nm_utils_create_dhcp_iaid(TRUE, (const guint8 *) iface, strlen(iface));
+        goto out_good;
+    }
+
+out_fail:
+    nm_assert(fail_reason);
+    if (!log_silent) {
+        _LOGW(LOGD_DEVICE | LOGD_DHCPX(IS_IPv4) | LOGD_IPX(IS_IPv4),
+              "ipv%c.dhcp-iaid: failure to generate IAID: %s. Using interface-name based IAID",
+              nm_utils_addr_family_to_char(addr_family),
+              fail_reason);
+    }
+    is_explicit = FALSE;
+    iface       = nm_device_get_ip_iface(self);
+    iaid        = nm_utils_create_dhcp_iaid(TRUE, (const guint8 *) iface, strlen(iface));
+out_good:
+    if (!log_silent) {
+        _LOGD(LOGD_DEVICE | LOGD_DHCPX(IS_IPv4) | LOGD_IPX(IS_IPv4),
+              "ipv%c.dhcp-iaid: using %u (0x%08x) IAID (str: '%s', explicit %d)",
+              nm_utils_addr_family_to_char(addr_family),
+              iaid,
+              iaid,
+              iaid_str,
+              is_explicit);
+    }
+    NM_SET_OUT(out_is_explicit, is_explicit);
+    return iaid;
+}
+
+static NMDhcpHostnameFlags
+_prop_get_ipvx_dhcp_hostname_flags(NMDevice *self, int addr_family)
+{
+    NMConnection *      connection;
+    NMSettingIPConfig * s_ip;
+    NMDhcpHostnameFlags flags;
+    gs_free_error GError *error = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_DHCP_HOSTNAME_FLAG_NONE);
+
+    connection = nm_device_get_applied_connection(self);
+    s_ip       = nm_connection_get_setting_ip_config(connection, addr_family);
+    g_return_val_if_fail(s_ip, NM_DHCP_HOSTNAME_FLAG_NONE);
+
+    if (!nm_setting_ip_config_get_dhcp_send_hostname(s_ip))
+        return NM_DHCP_HOSTNAME_FLAG_NONE;
+
+    flags = nm_setting_ip_config_get_dhcp_hostname_flags(s_ip);
+    if (flags != NM_DHCP_HOSTNAME_FLAG_NONE)
+        return flags;
+
+    flags = nm_config_data_get_connection_default_int64(
+        NM_CONFIG_GET_DATA,
+        NM_IS_IPv4(addr_family) ? NM_CON_DEFAULT("ipv4.dhcp-hostname-flags")
+                                : NM_CON_DEFAULT("ipv6.dhcp-hostname-flags"),
+        self,
+        0,
+        NM_DHCP_HOSTNAME_FLAG_FQDN_CLEAR_FLAGS,
+        0);
+
+    if (!_nm_utils_validate_dhcp_hostname_flags(flags, addr_family, &error)) {
+        _LOGW(LOGD_DEVICE,
+              "invalid global default value 0x%x for ipv%c.%s: %s",
+              (guint) flags,
+              nm_utils_addr_family_to_char(addr_family),
+              NM_SETTING_IP_CONFIG_DHCP_HOSTNAME_FLAGS,
+              error->message);
+        flags = NM_DHCP_HOSTNAME_FLAG_NONE;
+    }
+
+    if (flags != NM_DHCP_HOSTNAME_FLAG_NONE)
+        return flags;
+
+    if (NM_IS_IPv4(addr_family))
+        return NM_DHCP_HOSTNAME_FLAGS_FQDN_DEFAULT_IP4;
+    else
+        return NM_DHCP_HOSTNAME_FLAGS_FQDN_DEFAULT_IP6;
+}
+
+static const char *
+_prop_get_connection_mud_url(NMDevice *self, NMSettingConnection *s_con, char **out_mud_url)
+{
+    const char *  mud_url;
+    gs_free char *s = NULL;
+
+    nm_assert(out_mud_url && !*out_mud_url);
+
+    mud_url = nm_setting_connection_get_mud_url(s_con);
+
+    if (mud_url) {
+        if (nm_streq(mud_url, NM_CONNECTION_MUD_URL_NONE))
+            return NULL;
+        return mud_url;
+    }
+
+    s = nm_config_data_get_connection_default(NM_CONFIG_GET_DATA,
+                                              NM_CON_DEFAULT("connection.mud-url"),
+                                              self);
+    if (s) {
+        if (nm_streq(s, NM_CONNECTION_MUD_URL_NONE))
+            return NULL;
+        if (nm_sd_http_url_is_valid_https(s))
+            return (*out_mud_url = g_steal_pointer(&s));
+    }
+
+    return NULL;
+}
+
+static GBytes *
+_prop_get_ipv4_dhcp_client_id(NMDevice *self, NMConnection *connection, GBytes *hwaddr)
+{
+    NMSettingIPConfig *s_ip4;
+    const char *       client_id;
+    gs_free char *     client_id_default = NULL;
+    guint8 *           client_id_buf;
+    const char *       fail_reason;
+    guint8             hwaddr_bin_buf[NM_UTILS_HWADDR_LEN_MAX];
+    const guint8 *     hwaddr_bin;
+    int                arp_type;
+    gsize              hwaddr_len;
+    GBytes *           result;
+    gs_free char *     logstr1 = NULL;
+
+    s_ip4     = nm_connection_get_setting_ip4_config(connection);
+    client_id = nm_setting_ip4_config_get_dhcp_client_id(NM_SETTING_IP4_CONFIG(s_ip4));
+
+    if (!client_id) {
+        client_id_default =
+            nm_config_data_get_connection_default(NM_CONFIG_GET_DATA,
+                                                  NM_CON_DEFAULT("ipv4.dhcp-client-id"),
+                                                  self);
+        if (client_id_default && client_id_default[0]) {
+            /* a non-empty client-id is always valid, see nm_dhcp_utils_client_id_string_to_bytes().  */
+            client_id = client_id_default;
+        }
+    }
+
+    if (!client_id) {
+        _LOGD(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4,
+              "ipv4.dhcp-client-id: no explicit client-id configured");
+        return NULL;
+    }
+
+    if (nm_streq(client_id, "mac")) {
+        if (!hwaddr) {
+            fail_reason = "missing link-layer address";
+            goto out_fail;
+        }
+
+        hwaddr_bin = g_bytes_get_data(hwaddr, &hwaddr_len);
+        arp_type   = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len);
+        if (arp_type < 0) {
+            fail_reason = "unsupported link-layer address";
+            goto out_fail;
+        }
+
+        result = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_bin, hwaddr_len);
+        goto out_good;
+    }
+
+    if (nm_streq(client_id, "perm-mac")) {
+        const char *hwaddr_str;
+
+        hwaddr_str = nm_device_get_permanent_hw_address(self);
+        if (!hwaddr_str) {
+            fail_reason = "missing permanent link-layer address";
+            goto out_fail;
+        }
+
+        if (!_nm_utils_hwaddr_aton(hwaddr_str, hwaddr_bin_buf, sizeof(hwaddr_bin_buf), &hwaddr_len))
+            g_return_val_if_reached(NULL);
+
+        arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len);
+        if (arp_type < 0) {
+            fail_reason = "unsupported permanent link-layer address";
+            goto out_fail;
+        }
+
+        result = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_bin_buf, hwaddr_len);
+        goto out_good;
+    }
+
+    if (nm_streq(client_id, "duid")) {
+        guint32 iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET, connection, FALSE, NULL);
+
+        result = nm_utils_dhcp_client_id_systemd_node_specific(iaid);
+        goto out_good;
+    }
+
+    if (nm_streq(client_id, "ipv6-duid")) {
+        gs_unref_bytes GBytes *duid = NULL;
+        gboolean               iaid_is_explicit;
+        guint32                iaid;
+        const guint8 *         duid_arr;
+        gsize                  duid_len;
+
+        iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET, connection, FALSE, &iaid_is_explicit);
+        if (!iaid_is_explicit)
+            iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET6, connection, FALSE, &iaid_is_explicit);
+
+        duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, NULL);
+
+        nm_assert(duid);
+
+        duid_arr = g_bytes_get_data(duid, &duid_len);
+
+        nm_assert(duid_arr);
+        nm_assert(duid_len >= 2u + 1u);
+        nm_assert(duid_len <= 2u + 128u);
+
+        result = nm_utils_dhcp_client_id_duid(iaid, duid_arr, duid_len);
+        goto out_good;
+    }
+
+    if (nm_streq(client_id, "stable")) {
+        nm_auto_free_checksum GChecksum *sum = NULL;
+        guint8                           digest[NM_UTILS_CHECKSUM_LENGTH_SHA1];
+        NMUtilsStableType                stable_type;
+        const char *                     stable_id;
+        guint32                          salted_header;
+        const guint8 *                   host_id;
+        gsize                            host_id_len;
+
+        stable_id     = _prop_get_connection_stable_id(self, connection, &stable_type);
+        salted_header = htonl(2011610591 + stable_type);
+        nm_utils_host_id_get(&host_id, &host_id_len);
+
+        sum = g_checksum_new(G_CHECKSUM_SHA1);
+        g_checksum_update(sum, (const guchar *) &salted_header, sizeof(salted_header));
+        g_checksum_update(sum, (const guchar *) stable_id, strlen(stable_id) + 1);
+        g_checksum_update(sum, (const guchar *) host_id, host_id_len);
+        nm_utils_checksum_get_digest(sum, digest);
+
+        client_id_buf    = g_malloc(1 + 15);
+        client_id_buf[0] = 0;
+        memcpy(&client_id_buf[1], digest, 15);
+        result = g_bytes_new_take(client_id_buf, 1 + 15);
+        goto out_good;
+    }
+
+    result = nm_dhcp_utils_client_id_string_to_bytes(client_id);
+    goto out_good;
+
+out_fail:
+    nm_assert(fail_reason);
+    _LOGW(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4,
+          "ipv4.dhcp-client-id: failure to generate client id (%s). Use random client id",
+          fail_reason);
+    client_id_buf    = g_malloc(1 + 15);
+    client_id_buf[0] = 0;
+    nm_utils_random_bytes(&client_id_buf[1], 15);
+    result = g_bytes_new_take(client_id_buf, 1 + 15);
+
+out_good:
+    nm_assert(result);
+    _LOGD(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4,
+          "ipv4.dhcp-client-id: use \"%s\" client ID: %s",
+          client_id,
+          (logstr1 = nm_dhcp_utils_duid_to_string(result)));
+    return result;
+}
+
+static GBytes *
+_prop_get_ipv4_dhcp_vendor_class_identifier(NMDevice *self, NMSettingIP4Config *s_ip4)
+{
+    gs_free char *config_data_prop = NULL;
+    gs_free char *to_free          = NULL;
+    const char *  conn_prop;
+    GBytes *      bytes = NULL;
+    const char *  bin;
+    gsize         len;
+
+    conn_prop = nm_setting_ip4_config_get_dhcp_vendor_class_identifier(s_ip4);
+
+    if (!conn_prop) {
+        /* set in NetworkManager.conf ? */
+        config_data_prop = nm_config_data_get_connection_default(
+            NM_CONFIG_GET_DATA,
+            NM_CON_DEFAULT("ipv4.dhcp-vendor-class-identifier"),
+            self);
+
+        if (config_data_prop && nm_utils_validate_dhcp4_vendor_class_id(config_data_prop, NULL))
+            conn_prop = config_data_prop;
+    }
+
+    if (conn_prop) {
+        bin = nm_utils_buf_utf8safe_unescape(conn_prop,
+                                             NM_UTILS_STR_UTF8_SAFE_FLAG_NONE,
+                                             &len,
+                                             (gpointer *) &to_free);
+        if (to_free)
+            bytes = g_bytes_new_take(g_steal_pointer(&to_free), len);
+        else
+            bytes = g_bytes_new(bin, len);
+    }
+
+    return bytes;
+}
+
+static NMSettingIP6ConfigPrivacy
+_prop_get_ipv6_ip6_privacy(NMDevice *self)
+{
+    NMSettingIP6ConfigPrivacy ip6_privacy;
+    NMConnection *            connection;
+
+    g_return_val_if_fail(self, NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+
+    /* 1.) First look at the per-connection setting. If it is not -1 (unknown),
+     * use it. */
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection);
+
+        if (s_ip6) {
+            ip6_privacy = nm_setting_ip6_config_get_ip6_privacy(NM_SETTING_IP6_CONFIG(s_ip6));
+            ip6_privacy = _ip6_privacy_clamp(ip6_privacy);
+            if (ip6_privacy != NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN)
+                return ip6_privacy;
+        }
+    }
+
+    /* 2.) use the default value from the configuration. */
+    ip6_privacy =
+        nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                    NM_CON_DEFAULT("ipv6.ip6-privacy"),
+                                                    self,
+                                                    NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN,
+                                                    NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR,
+                                                    NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+    if (ip6_privacy != NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN)
+        return ip6_privacy;
+
+    if (!nm_device_get_ip_ifindex(self))
+        return NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
+
+    /* 3.) No valid default-value configured. Fallback to reading sysctl.
+     *
+     * Instead of reading static config files in /etc, just read the current sysctl value.
+     * This works as NM only writes to "/proc/sys/net/ipv6/conf/IFNAME/use_tempaddr", but leaves
+     * the "default" entry untouched. */
+    ip6_privacy = nm_platform_sysctl_get_int32(
+        nm_device_get_platform(self),
+        NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv6/conf/default/use_tempaddr"),
+        NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+    return _ip6_privacy_clamp(ip6_privacy);
+}
+
+static const char *
+_prop_get_x_cloned_mac_address(NMDevice *    self,
+                               NMConnection *connection,
+                               gboolean      is_wifi,
+                               char **       out_addr)
+{
+    NMSetting * setting;
+    const char *addr = NULL;
+
+    nm_assert(out_addr && !*out_addr);
+
+    setting = nm_connection_get_setting(connection,
+                                        is_wifi ? NM_TYPE_SETTING_WIRELESS : NM_TYPE_SETTING_WIRED);
+    if (setting) {
+        addr = is_wifi ? nm_setting_wireless_get_cloned_mac_address((NMSettingWireless *) setting)
+                       : nm_setting_wired_get_cloned_mac_address((NMSettingWired *) setting);
+    }
+
+    if (!addr) {
+        gs_free char *a = NULL;
+
+        a = nm_config_data_get_connection_default(
+            NM_CONFIG_GET_DATA,
+            is_wifi ? NM_CON_DEFAULT("wifi.cloned-mac-address")
+                    : NM_CON_DEFAULT("ethernet.cloned-mac-address"),
+            self);
+
+        addr = NM_CLONED_MAC_PRESERVE;
+
+        if (!a) {
+            if (is_wifi) {
+                NMSettingMacRandomization v;
+
+                /* for backward compatibility, read the deprecated wifi.mac-address-randomization setting. */
+                a = nm_config_data_get_connection_default(
+                    NM_CONFIG_GET_DATA,
+                    NM_CON_DEFAULT("wifi.mac-address-randomization"),
+                    self);
+                v = _nm_utils_ascii_str_to_int64(a,
+                                                 10,
+                                                 NM_SETTING_MAC_RANDOMIZATION_DEFAULT,
+                                                 NM_SETTING_MAC_RANDOMIZATION_ALWAYS,
+                                                 NM_SETTING_MAC_RANDOMIZATION_DEFAULT);
+                if (v == NM_SETTING_MAC_RANDOMIZATION_ALWAYS)
+                    addr = NM_CLONED_MAC_RANDOM;
+            }
+        } else if (NM_CLONED_MAC_IS_SPECIAL(a) || nm_utils_hwaddr_valid(a, ETH_ALEN))
+            addr = *out_addr = g_steal_pointer(&a);
+    }
+
+    return addr;
+}
+
+static const char *
+_prop_get_x_generate_mac_address_mask(NMDevice *    self,
+                                      NMConnection *connection,
+                                      gboolean      is_wifi,
+                                      char **       out_value)
+{
+    NMSetting * setting;
+    const char *value = NULL;
+    char *      a;
+
+    nm_assert(out_value && !*out_value);
+
+    setting = nm_connection_get_setting(connection,
+                                        is_wifi ? NM_TYPE_SETTING_WIRELESS : NM_TYPE_SETTING_WIRED);
+    if (setting) {
+        value =
+            is_wifi
+                ? nm_setting_wireless_get_generate_mac_address_mask((NMSettingWireless *) setting)
+                : nm_setting_wired_get_generate_mac_address_mask((NMSettingWired *) setting);
+        if (value)
+            return value;
+    }
+
+    a = nm_config_data_get_connection_default(
+        NM_CONFIG_GET_DATA,
+        is_wifi ? NM_CON_DEFAULT("wifi.generate-mac-address-mask")
+                : NM_CON_DEFAULT("ethernet.generate-mac-address-mask"),
+        self);
+    if (!a)
+        return NULL;
+    *out_value = a;
+    return a;
+}
+
+/*****************************************************************************/
+
+static void
+_ethtool_features_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
+{
+    gs_free NMEthtoolFeatureStates *features;
+
+    features = g_steal_pointer(&ethtool_state->features);
+
+    if (!nm_platform_ethtool_set_features(platform,
+                                          ethtool_state->ifindex,
+                                          features,
+                                          ethtool_state->requested,
+                                          FALSE))
+        _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more offload features");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: offload features successfully reset");
+}
+
+static void
+_ethtool_features_set(NMDevice *        self,
+                      NMPlatform *      platform,
+                      EthtoolState *    ethtool_state,
+                      NMSettingEthtool *s_ethtool)
+{
+    gs_free NMEthtoolFeatureStates *features = NULL;
+
+    if (ethtool_state->features)
+        _ethtool_features_reset(self, platform, ethtool_state);
+
+    if (nm_setting_ethtool_init_features(s_ethtool, ethtool_state->requested) == 0)
+        return;
+
+    features = nm_platform_ethtool_get_link_features(platform, ethtool_state->ifindex);
+    if (!features) {
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting offload features (cannot read features)");
+        return;
+    }
+
+    if (!nm_platform_ethtool_set_features(platform,
+                                          ethtool_state->ifindex,
+                                          features,
+                                          ethtool_state->requested,
+                                          TRUE))
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting one or more offload features");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: offload features successfully set");
+
+    ethtool_state->features = g_steal_pointer(&features);
+}
+
+static void
+_ethtool_coalesce_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
+{
+    gs_free NMEthtoolCoalesceState *coalesce = NULL;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(ethtool_state);
+
+    coalesce = g_steal_pointer(&ethtool_state->coalesce);
+    if (!coalesce)
+        return;
+
+    if (!nm_platform_ethtool_set_coalesce(platform, ethtool_state->ifindex, coalesce))
+        _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more coalesce settings");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: coalesce settings successfully reset");
+}
+
+static void
+_ethtool_coalesce_set(NMDevice *        self,
+                      NMPlatform *      platform,
+                      EthtoolState *    ethtool_state,
+                      NMSettingEthtool *s_ethtool)
+{
+    NMEthtoolCoalesceState coalesce_old;
+    NMEthtoolCoalesceState coalesce_new;
+    gboolean               has_old = FALSE;
+    GHashTable *           hash;
+    GHashTableIter         iter;
+    const char *           name;
+    GVariant *             variant;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool));
+    nm_assert(ethtool_state);
+    nm_assert(!ethtool_state->coalesce);
+
+    hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE);
+    if (!hash)
+        return;
+
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) {
+        NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name);
+
+        if (!nm_ethtool_id_is_coalesce(ethtool_id))
+            continue;
+
+        if (!has_old) {
+            if (!nm_platform_ethtool_get_link_coalesce(platform,
+                                                       ethtool_state->ifindex,
+                                                       &coalesce_old)) {
+                _LOGW(LOGD_DEVICE, "ethtool: failure getting coalesce settings (cannot read)");
+                return;
+            }
+            has_old      = TRUE;
+            coalesce_new = coalesce_old;
+        }
+
+        nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32));
+        coalesce_new.s[_NM_ETHTOOL_ID_COALESCE_AS_IDX(ethtool_id)] = g_variant_get_uint32(variant);
+    }
+
+    if (!has_old)
+        return;
+
+    ethtool_state->coalesce = nm_memdup(&coalesce_old, sizeof(coalesce_old));
+
+    if (!nm_platform_ethtool_set_coalesce(platform, ethtool_state->ifindex, &coalesce_new)) {
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting coalesce settings");
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "ethtool: coalesce settings successfully set");
+}
+
+static void
+_ethtool_ring_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
+{
+    gs_free NMEthtoolRingState *ring = NULL;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(ethtool_state);
+
+    ring = g_steal_pointer(&ethtool_state->ring);
+    if (!ring)
+        return;
+
+    if (!nm_platform_ethtool_set_ring(platform, ethtool_state->ifindex, ring))
+        _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more ring settings");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: ring settings successfully reset");
+}
+
+static void
+_ethtool_ring_set(NMDevice *        self,
+                  NMPlatform *      platform,
+                  EthtoolState *    ethtool_state,
+                  NMSettingEthtool *s_ethtool)
+{
+    NMEthtoolRingState ring_old;
+    NMEthtoolRingState ring_new;
+    GHashTable *       hash;
+    GHashTableIter     iter;
+    const char *       name;
+    GVariant *         variant;
+    gboolean           has_old = FALSE;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool));
+    nm_assert(ethtool_state);
+    nm_assert(!ethtool_state->ring);
+
+    hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE);
+    if (!hash)
+        return;
+
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) {
+        NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name);
+        guint32     u32;
+
+        if (!nm_ethtool_id_is_ring(ethtool_id))
+            continue;
+
+        nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32));
+
+        if (!has_old) {
+            if (!nm_platform_ethtool_get_link_ring(platform, ethtool_state->ifindex, &ring_old)) {
+                _LOGW(LOGD_DEVICE,
+                      "ethtool: failure setting ring options (cannot read existing setting)");
+                return;
+            }
+            has_old  = TRUE;
+            ring_new = ring_old;
+        }
+
+        u32 = g_variant_get_uint32(variant);
+
+        switch (ethtool_id) {
+        case NM_ETHTOOL_ID_RING_RX:
+            ring_new.rx_pending = u32;
+            break;
+        case NM_ETHTOOL_ID_RING_RX_JUMBO:
+            ring_new.rx_jumbo_pending = u32;
+            break;
+        case NM_ETHTOOL_ID_RING_RX_MINI:
+            ring_new.rx_mini_pending = u32;
+            break;
+        case NM_ETHTOOL_ID_RING_TX:
+            ring_new.tx_pending = u32;
+            break;
+        default:
+            nm_assert_not_reached();
+        }
+    }
+
+    if (!has_old)
+        return;
+
+    ethtool_state->ring = nm_memdup(&ring_old, sizeof(ring_old));
+
+    if (!nm_platform_ethtool_set_ring(platform, ethtool_state->ifindex, &ring_new)) {
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting ring settings");
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "ethtool: ring settings successfully set");
+}
+
+static void
+_ethtool_state_reset(NMDevice *self)
+{
+    NMPlatform *     platform           = nm_device_get_platform(self);
+    NMDevicePrivate *priv               = NM_DEVICE_GET_PRIVATE(self);
+    gs_free EthtoolState *ethtool_state = g_steal_pointer(&priv->ethtool_state);
+
+    if (!ethtool_state)
+        return;
+
+    if (ethtool_state->features)
+        _ethtool_features_reset(self, platform, ethtool_state);
+    if (ethtool_state->coalesce)
+        _ethtool_coalesce_reset(self, platform, ethtool_state);
+    if (ethtool_state->ring)
+        _ethtool_ring_reset(self, platform, ethtool_state);
+}
+
+static void
+_ethtool_state_set(NMDevice *self)
+{
+    int               ifindex;
+    NMPlatform *      platform;
+    NMConnection *    connection;
+    NMSettingEthtool *s_ethtool;
+    gs_free EthtoolState *ethtool_state = NULL;
+    NMDevicePrivate *     priv          = NM_DEVICE_GET_PRIVATE(self);
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        return;
+
+    platform = nm_device_get_platform(self);
+    nm_assert(platform);
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        return;
+
+    s_ethtool = NM_SETTING_ETHTOOL(nm_connection_get_setting(connection, NM_TYPE_SETTING_ETHTOOL));
+    if (!s_ethtool)
+        return;
+
+    ethtool_state          = g_new0(EthtoolState, 1);
+    ethtool_state->ifindex = ifindex;
+
+    _ethtool_features_set(self, platform, ethtool_state, s_ethtool);
+    _ethtool_coalesce_set(self, platform, ethtool_state, s_ethtool);
+    _ethtool_ring_set(self, platform, ethtool_state, s_ethtool);
+
+    if (ethtool_state->features || ethtool_state->coalesce || ethtool_state->ring)
+        priv->ethtool_state = g_steal_pointer(&ethtool_state);
+}
+
+/*****************************************************************************/
+
+static gboolean
+is_loopback(NMDevice *self)
+{
+    return NM_IS_DEVICE_GENERIC(self) && NM_DEVICE_GET_PRIVATE(self)->ifindex == 1;
+}
+
+gboolean
+nm_device_is_vpn(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    /* NetworkManager currently treats VPN connections (loaded from NetworkManager VPN plugins)
+     * differently. Those are considered VPNs.
+     * However, some native device types may also be considered VPNs...
+     *
+     * We should avoid distinguishing between is-vpn and "regular" devices. Is an (unencrypted)
+     * IP tunnel a VPN? Is MACSec on top of an IP tunnel a VPN?
+     * Sometimes we differentiate, but avoid unless reasonable. */
+
+    return NM_IS_DEVICE_WIREGUARD(self);
+}
+
+NMSettings *
+nm_device_get_settings(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->settings;
+}
+
+NMManager *
+nm_device_get_manager(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->manager;
+}
+
+NMNetns *
+nm_device_get_netns(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->netns;
+}
+
+NMDedupMultiIndex *
+nm_device_get_multi_index(NMDevice *self)
+{
+    return nm_netns_get_multi_idx(nm_device_get_netns(self));
+}
+
+NMPlatform *
+nm_device_get_platform(NMDevice *self)
+{
+    return nm_netns_get_platform(nm_device_get_netns(self));
+}
+
+static NMConnectivity *
+concheck_get_mgr(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (G_UNLIKELY(!priv->concheck_mgr))
+        priv->concheck_mgr = g_object_ref(nm_connectivity_get());
+    return priv->concheck_mgr;
+}
+
+NMIP4Config *
+nm_device_ip4_config_new(NMDevice *self)
+{
+    return nm_ip4_config_new(nm_device_get_multi_index(self), nm_device_get_ip_ifindex(self));
+}
+
+NMIP6Config *
+nm_device_ip6_config_new(NMDevice *self)
+{
+    return nm_ip6_config_new(nm_device_get_multi_index(self), nm_device_get_ip_ifindex(self));
+}
+
+NMIPConfig *
+nm_device_ip_config_new(NMDevice *self, int addr_family)
+{
+    nm_assert_addr_family(addr_family);
+
+    return NM_IS_IPv4(addr_family) ? (gpointer) nm_device_ip4_config_new(self)
+                                   : (gpointer) nm_device_ip6_config_new(self);
+}
+
+NML3ConfigData *
+nm_device_create_l3_config_data(NMDevice *self)
+{
+    int ifindex;
+
+    nm_assert(NM_IS_DEVICE(self));
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        g_return_val_if_reached(NULL);
+
+    return nm_l3_config_data_new(nm_device_get_multi_index(self), ifindex);
+}
+
+static void
+applied_config_clear(AppliedConfig *config)
+{
+    g_clear_object(&config->current);
+    g_clear_object(&config->orig);
+}
+
+static void
+applied_config_init(AppliedConfig *config, gpointer ip_config)
+{
+    nm_assert(!ip_config || (!config->orig && !config->current)
+              || nm_ip_config_get_addr_family(ip_config)
+                     == nm_ip_config_get_addr_family(config->orig ?: config->current));
+    nm_assert(!ip_config || NM_IS_IP_CONFIG(ip_config));
+
+    nm_g_object_ref(ip_config);
+    applied_config_clear(config);
+    config->orig = ip_config;
+}
+
+static void
+applied_config_init_new(AppliedConfig *config, NMDevice *self, int addr_family)
+{
+    gs_unref_object NMIPConfig *c = nm_device_ip_config_new(self, addr_family);
+
+    applied_config_init(config, c);
+}
+
+static NMIPConfig *
+applied_config_get_current(AppliedConfig *config)
+{
+    return config->current ?: config->orig;
+}
+
+static void
+applied_config_add_address(AppliedConfig *config, const NMPlatformIPAddress *address)
+{
+    if (config->orig)
+        nm_ip_config_add_address(config->orig, address);
+    else
+        nm_assert(!config->current);
+
+    if (config->current)
+        nm_ip_config_add_address(config->current, address);
+}
+
+static void
+applied_config_add_nameserver(AppliedConfig *config, const NMIPAddr *ns)
+{
+    if (config->orig)
+        nm_ip_config_add_nameserver(config->orig, ns);
+    else
+        nm_assert(!config->current);
+
+    if (config->current)
+        nm_ip_config_add_nameserver(config->current, ns);
+}
+
+static void
+applied_config_add_search(AppliedConfig *config, const char *new)
+{
+    if (config->orig)
+        nm_ip_config_add_search(config->orig, new);
+    else
+        nm_assert(!config->current);
+
+    if (config->current)
+        nm_ip_config_add_search(config->current, new);
+}
+
+static void
+applied_config_reset_searches(AppliedConfig *config)
+{
+    if (config->orig)
+        nm_ip_config_reset_searches(config->orig);
+    else
+        nm_assert(!config->current);
+
+    if (config->current)
+        nm_ip_config_reset_searches(config->current);
+}
+
+static void
+applied_config_reset_nameservers(AppliedConfig *config)
+{
+    if (config->orig)
+        nm_ip_config_reset_nameservers(config->orig);
+    else
+        nm_assert(!config->current);
+
+    if (config->current)
+        nm_ip_config_reset_nameservers(config->current);
+}
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    _sys_iface_state_to_str,
+    NMDeviceSysIfaceState,
+    NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT("unknown"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_EXTERNAL, "external"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_ASSUME, "assume"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_MANAGED, "managed"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_SYS_IFACE_STATE_REMOVED, "removed"), );
+
+NMDeviceSysIfaceState
+nm_device_sys_iface_state_get(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_SYS_IFACE_STATE_EXTERNAL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->sys_iface_state;
+}
+
+gboolean
+nm_device_sys_iface_state_is_external(NMDevice *self)
+{
+    return NM_IN_SET(nm_device_sys_iface_state_get(self), NM_DEVICE_SYS_IFACE_STATE_EXTERNAL);
+}
+
+gboolean
+nm_device_sys_iface_state_is_external_or_assume(NMDevice *self)
+{
+    return NM_IN_SET(nm_device_sys_iface_state_get(self),
+                     NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+                     NM_DEVICE_SYS_IFACE_STATE_ASSUME);
+}
+
+void
+nm_device_sys_iface_state_set(NMDevice *self, NMDeviceSysIfaceState sys_iface_state)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IN_SET(sys_iface_state,
+                               NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+                               NM_DEVICE_SYS_IFACE_STATE_ASSUME,
+                               NM_DEVICE_SYS_IFACE_STATE_MANAGED,
+                               NM_DEVICE_SYS_IFACE_STATE_REMOVED));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    if (priv->sys_iface_state != sys_iface_state) {
+        _LOGT(LOGD_DEVICE,
+              "sys-iface-state: %s -> %s",
+              _sys_iface_state_to_str(priv->sys_iface_state),
+              _sys_iface_state_to_str(sys_iface_state));
+        priv->sys_iface_state_ = sys_iface_state;
+    }
+
+    /* this function only sets a flag, no immediate actions are initiated.
+     *
+     * If you change this, make sure that all callers are fine with such actions. */
+
+    nm_assert(priv->sys_iface_state == sys_iface_state);
+}
+
+static void
+_active_connection_set_state_flags_full(NMDevice *             self,
+                                        NMActivationStateFlags flags,
+                                        NMActivationStateFlags mask)
+{
+    NMActiveConnection *ac;
+
+    ac = NM_ACTIVE_CONNECTION(nm_device_get_act_request(self));
+    if (ac)
+        nm_active_connection_set_state_flags_full(ac, flags, mask);
+}
+
+static void
+_active_connection_set_state_flags(NMDevice *self, NMActivationStateFlags flags)
+{
+    _active_connection_set_state_flags_full(self, flags, flags);
+}
+
+/*****************************************************************************/
+
+void
+nm_device_assume_state_get(NMDevice *   self,
+                           gboolean *   out_assume_state_guess_assume,
+                           const char **out_assume_state_connection_uuid)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    NM_SET_OUT(out_assume_state_guess_assume, priv->assume_state_guess_assume);
+    NM_SET_OUT(out_assume_state_connection_uuid, priv->assume_state_connection_uuid);
+}
+
+static void
+_assume_state_set(NMDevice *  self,
+                  gboolean    assume_state_guess_assume,
+                  const char *assume_state_connection_uuid)
+{
+    NMDevicePrivate *priv;
+
+    nm_assert(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    if (priv->assume_state_guess_assume == !!assume_state_guess_assume
+        && nm_streq0(priv->assume_state_connection_uuid, assume_state_connection_uuid))
+        return;
+
+    _LOGD(LOGD_DEVICE,
+          "assume-state: set guess-assume=%c, connection=%s%s%s",
+          assume_state_guess_assume ? '1' : '0',
+          NM_PRINT_FMT_QUOTE_STRING(assume_state_connection_uuid));
+    priv->assume_state_guess_assume = assume_state_guess_assume;
+    g_free(priv->assume_state_connection_uuid);
+    priv->assume_state_connection_uuid = g_strdup(assume_state_connection_uuid);
+}
+
+void
+nm_device_assume_state_reset(NMDevice *self)
+{
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    _assume_state_set(self, FALSE, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+init_ip_config_dns_priority(NMDevice *self, NMIPConfig *config)
+{
+    const char *property;
+    int         priority;
+
+    property = (nm_ip_config_get_addr_family(config) == AF_INET)
+                   ? NM_CON_DEFAULT("ipv4.dns-priority")
+                   : NM_CON_DEFAULT("ipv6.dns-priority");
+
+    priority = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                           property,
+                                                           self,
+                                                           G_MININT,
+                                                           G_MAXINT,
+                                                           0);
+
+    if (priority == 0) {
+        priority =
+            nm_device_is_vpn(self) ? NM_DNS_PRIORITY_DEFAULT_VPN : NM_DNS_PRIORITY_DEFAULT_NORMAL;
+    }
+
+    nm_ip_config_set_dns_priority(config, priority);
+}
+
+/*****************************************************************************/
+
+static char *
+nm_device_sysctl_ip_conf_get(NMDevice *self, int addr_family, const char *property)
+{
+    const char *ifname;
+
+    nm_assert_addr_family(addr_family);
+
+    ifname = nm_device_get_ip_iface_from_platform(self);
+    if (!ifname)
+        return NULL;
+    return nm_platform_sysctl_ip_conf_get(nm_device_get_platform(self),
+                                          addr_family,
+                                          ifname,
+                                          property);
+}
+
+static gint64
+nm_device_sysctl_ip_conf_get_int_checked(NMDevice *  self,
+                                         int         addr_family,
+                                         const char *property,
+                                         guint       base,
+                                         gint64      min,
+                                         gint64      max,
+                                         gint64      fallback)
+{
+    const char *ifname;
+
+    nm_assert_addr_family(addr_family);
+
+    ifname = nm_device_get_ip_iface_from_platform(self);
+    if (!ifname) {
+        errno = EINVAL;
+        return fallback;
+    }
+    return nm_platform_sysctl_ip_conf_get_int_checked(nm_device_get_platform(self),
+                                                      addr_family,
+                                                      ifname,
+                                                      property,
+                                                      base,
+                                                      min,
+                                                      max,
+                                                      fallback);
+}
+
+static void
+set_ipv6_token(NMDevice *self, NMUtilsIPv6IfaceId iid, const char *token_str)
+{
+    NMPlatform *          platform;
+    int                   ifindex;
+    const NMPlatformLink *link;
+    char                  buf[32];
+    gint64                val;
+
+    /* Setting the kernel token is not strictly necessary as the
+     * IPv6 address is generated in userspace. However it is
+     * convenient so that users can see the token with iproute
+     * ('ip token'). */
+    platform = nm_device_get_platform(self);
+    ifindex  = nm_device_get_ip_ifindex(self);
+    link     = nm_platform_link_get(platform, ifindex);
+
+    if (link && link->inet6_token.id == iid.id) {
+        _LOGT(LOGD_DEVICE | LOGD_IP6, "token %s already set", token_str);
+        return;
+    }
+
+    /* The kernel allows setting a token only when 'accept_ra'
+     * is 1: temporarily flip it if necessary; unfortunately
+     * this will also generate an additional Router Solicitation
+     * from kernel. */
+    val = nm_device_sysctl_ip_conf_get_int_checked(self,
+                                                   AF_INET6,
+                                                   "accept_ra",
+                                                   10,
+                                                   G_MININT32,
+                                                   G_MAXINT32,
+                                                   1);
+    if (val != 1)
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "1");
+
+    nm_platform_link_set_ipv6_token(platform, ifindex, iid);
+
+    if (val != 1) {
+        nm_sprintf_buf(buf, "%d", (int) val);
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", buf);
+    }
+}
+
+gboolean
+nm_device_sysctl_ip_conf_set(NMDevice *  self,
+                             int         addr_family,
+                             const char *property,
+                             const char *value)
+{
+    NMPlatform *  platform      = nm_device_get_platform(self);
+    gs_free char *value_to_free = NULL;
+    const char *  ifname;
+
+    nm_assert_addr_family(addr_family);
+
+    ifname = nm_device_get_ip_iface_from_platform(self);
+    if (!ifname)
+        return FALSE;
+
+    if (!value) {
+        /* Set to a default value when we've got a NULL @value. */
+        value_to_free = nm_platform_sysctl_ip_conf_get(platform, addr_family, "default", property);
+        value         = value_to_free;
+        if (!value)
+            return FALSE;
+    }
+
+    return nm_platform_sysctl_ip_conf_set(platform, addr_family, ifname, property, value);
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_device_has_capability(NMDevice *self, NMDeviceCapabilities caps)
+{
+    return NM_FLAGS_ANY(NM_DEVICE_GET_PRIVATE(self)->capabilities, caps);
+}
+
+static void
+_add_capabilities(NMDevice *self, NMDeviceCapabilities capabilities)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!NM_FLAGS_ALL(priv->capabilities, capabilities)) {
+        priv->capabilities |= capabilities;
+        _notify(self, PROP_CAPABILITIES);
+    }
+}
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(_ip_state_to_string,
+                                  NMDeviceIPState,
+                                  NM_UTILS_LOOKUP_DEFAULT_WARN("unknown"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_NONE, "none"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_WAIT, "wait"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_CONF, "conf"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_DONE, "done"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_IP_STATE_FAIL, "fail"), );
+
+static void
+_set_ip_state(NMDevice *self, int addr_family, NMDeviceIPState new_state)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    nm_assert_addr_family(addr_family);
+
+    if (priv->ip_state_x[IS_IPv4] == new_state)
+        return;
+
+    _LOGT(LOGD_DEVICE,
+          "ip%c-state: set to %d (%s)",
+          nm_utils_addr_family_to_char(addr_family),
+          (int) new_state,
+          _ip_state_to_string(new_state));
+
+    priv->ip_state_x_[IS_IPv4] = new_state;
+
+    if (new_state == NM_DEVICE_IP_STATE_DONE) {
+        /* we only set the IPx_READY flag once we reach NM_DEVICE_IP_STATE_DONE state. We don't
+         * ever clear it, even if we later enter NM_DEVICE_IP_STATE_FAIL state.
+         *
+         * This is not documented/guaranteed behavior, but seems to make sense for now. */
+        _active_connection_set_state_flags(self,
+                                           NM_IS_IPv4(addr_family)
+                                               ? NM_ACTIVATION_STATE_FLAG_IP4_READY
+                                               : NM_ACTIVATION_STATE_FLAG_IP6_READY);
+    }
+}
+
+/*****************************************************************************/
+
+const char *
+nm_device_get_udi(NMDevice *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->udi;
+}
+
+const char *
+nm_device_get_iface(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->iface;
+}
+
+static gboolean
+_set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    int *            p_ifindex;
+
+    if (ifindex < 0)
+        ifindex = 0;
+
+    p_ifindex = is_ip_ifindex ? &priv->ip_ifindex_ : &priv->ifindex_;
+
+    if (*p_ifindex == ifindex)
+        return FALSE;
+
+    *p_ifindex = ifindex;
+
+    _LOGD(LOGD_DEVICE, "ifindex: set %sifindex %d", is_ip_ifindex ? "ip-" : "", ifindex);
+
+    if (!is_ip_ifindex)
+        _notify(self, PROP_IFINDEX);
+
+    if (priv->manager)
+        nm_manager_emit_device_ifindex_changed(priv->manager, self);
+    return TRUE;
+}
+
+/**
+ * nm_device_take_over_link:
+ * @self: the #NMDevice
+ * @ifindex: a ifindex
+ * @old_name: (transfer full): on return, the name of the old link, if
+ *   the link was renamed
+ * @error: location to store error, or %NULL
+ *
+ * Given an existing link, move it under the control of a device. In
+ * particular, the link will be renamed to match the device name. If the
+ * link was renamed, the old name is returned in @old_name.
+ *
+ * Returns: %TRUE if the device took control of the link, %FALSE otherwise
+ */
+gboolean
+nm_device_take_over_link(NMDevice *self, int ifindex, char **old_name, GError **error)
+{
+    NMDevicePrivate *     priv = NM_DEVICE_GET_PRIVATE(self);
+    const NMPlatformLink *plink;
+    NMPlatform *          platform;
+
+    nm_assert(ifindex > 0);
+    NM_SET_OUT(old_name, NULL);
+
+    if (priv->ifindex > 0 && priv->ifindex != ifindex) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "the device already has ifindex %d",
+                           priv->ifindex);
+        return FALSE;
+    }
+
+    platform = nm_device_get_platform(self);
+    plink    = nm_platform_link_get(platform, ifindex);
+    if (!plink) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "link %d not found", ifindex);
+        return FALSE;
+    }
+
+    if (!nm_streq(plink->name, nm_device_get_iface(self))) {
+        gboolean      up;
+        gboolean      success;
+        gs_free char *name = NULL;
+
+        up   = NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP);
+        name = g_strdup(plink->name);
+
+        /* Rename the link to the device ifname */
+        if (up)
+            nm_platform_link_set_down(platform, ifindex);
+        success = nm_platform_link_set_name(platform, ifindex, nm_device_get_iface(self));
+        if (up)
+            nm_platform_link_set_up(platform, ifindex, NULL);
+
+        if (!success) {
+            nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "failure renaming link %d", ifindex);
+            return FALSE;
+        }
+
+        NM_SET_OUT(old_name, g_steal_pointer(&name));
+    }
+
+    _set_ifindex(self, ifindex, FALSE);
+
+    return TRUE;
+}
+
+int
+nm_device_get_ifindex(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), 0);
+
+    return NM_DEVICE_GET_PRIVATE(self)->ifindex;
+}
+
+/**
+ * nm_device_is_software:
+ * @self: the #NMDevice
+ *
+ * Indicates if the device is a software-based virtual device without
+ * backing hardware, which can be added and removed programmatically.
+ *
+ * Returns: %TRUE if the device is a software-based device
+ */
+gboolean
+nm_device_is_software(NMDevice *self)
+{
+    return NM_FLAGS_HAS(NM_DEVICE_GET_PRIVATE(self)->capabilities, NM_DEVICE_CAP_IS_SOFTWARE);
+}
+
+/**
+ * nm_device_is_real:
+ * @self: the #NMDevice
+ *
+ * Returns: %TRUE if the device exists, %FALSE if the device is a placeholder
+ */
+gboolean
+nm_device_is_real(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    return NM_DEVICE_GET_PRIVATE(self)->real;
+}
+
+const char *
+nm_device_get_ip_iface(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(self != NULL, NULL);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    /* If it's not set, default to iface */
+    return priv->ip_iface ?: priv->iface;
+}
+
+const char *
+nm_device_get_ip_iface_from_platform(NMDevice *self)
+{
+    int ifindex;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        return NULL;
+
+    return nm_platform_link_get_name(nm_device_get_platform(self), ifindex);
+}
+
+int
+nm_device_get_ip_ifindex(const NMDevice *self)
+{
+    const NMDevicePrivate *priv;
+
+    g_return_val_if_fail(self != NULL, 0);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    /* If it's not set, default to ifindex */
+    return priv->ip_iface ? priv->ip_ifindex : priv->ifindex;
+}
+
+static void
+_set_ip_ifindex(NMDevice *self, int ifindex, const char *ifname)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform *     platform;
+    gboolean         eq_name;
+
+    /* normalize arguments */
+    if (ifindex <= 0) {
+        ifindex = 0;
+        ifname  = NULL;
+    }
+
+    eq_name = nm_streq0(priv->ip_iface, ifname);
+
+    if (eq_name && priv->ip_ifindex == ifindex)
+        return;
+
+    _LOGD(LOGD_DEVICE,
+          "ip-ifindex: update ip-interface to %s%s%s, ifindex %d",
+          NM_PRINT_FMT_QUOTE_STRING(ifname),
+          ifindex);
+
+    _set_ifindex(self, ifindex, TRUE);
+
+    if (!eq_name) {
+        g_free(priv->ip_iface_);
+        priv->ip_iface_ = g_strdup(ifname);
+        _notify(self, PROP_IP_IFACE);
+    }
+
+    if (priv->ip_ifindex > 0) {
+        platform = nm_device_get_platform(self);
+
+        nm_platform_process_events_ensure_link(platform, priv->ip_ifindex, priv->ip_iface);
+
+        if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL))
+            nm_platform_link_set_user_ipv6ll_enabled(platform, priv->ip_ifindex, TRUE);
+
+        if (!nm_platform_link_is_up(platform, priv->ip_ifindex))
+            nm_platform_link_set_up(platform, priv->ip_ifindex, NULL);
+    }
+
+    /* We don't care about any saved values from the old iface */
+    g_hash_table_remove_all(priv->ip6_saved_properties);
+}
+
+gboolean
+nm_device_set_ip_ifindex(NMDevice *self, int ifindex)
+{
+    char        ifname_buf[IFNAMSIZ];
+    const char *ifname = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(nm_device_is_activating(self), FALSE);
+
+    if (ifindex > 0) {
+        ifname = nm_platform_if_indextoname(nm_device_get_platform(self), ifindex, ifname_buf);
+        if (!ifname)
+            _LOGW(LOGD_DEVICE, "ip-ifindex: ifindex %d not found", ifindex);
+    }
+
+    _set_ip_ifindex(self, ifindex, ifname);
+    return ifindex > 0;
+}
+
+/**
+ * nm_device_set_ip_iface:
+ * @self: the #NMDevice
+ * @ifname: the new IP interface name
+ *
+ * Updates the IP interface name and possibly the ifindex.
+ *
+ * Returns: %TRUE if an interface with name @ifname exists,
+ *   and %FALSE, if @ifname is %NULL or no such interface exists.
+ */
+gboolean
+nm_device_set_ip_iface(NMDevice *self, const char *ifname)
+{
+    int ifindex = 0;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(nm_device_is_activating(self), FALSE);
+
+    if (ifname) {
+        ifindex = nm_platform_if_nametoindex(nm_device_get_platform(self), ifname);
+        if (ifindex <= 0)
+            _LOGW(LOGD_DEVICE, "ip-ifindex: ifname %s not found", ifname);
+    }
+
+    _set_ip_ifindex(self, ifindex, ifname);
+    return ifindex > 0;
+}
+
+/*****************************************************************************/
+
+int
+nm_device_parent_get_ifindex(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), 0);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    return priv->parent_ifindex;
+}
+
+NMDevice *
+nm_device_parent_get_device(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->parent_device.obj;
+}
+
+static void
+parent_changed_notify(NMDevice *self,
+                      int       old_ifindex,
+                      NMDevice *old_parent,
+                      int       new_ifindex,
+                      NMDevice *new_parent)
+{
+    /* empty handler to allow subclasses to always chain up the virtual function. */
+}
+
+static gboolean
+_parent_set_ifindex(NMDevice *self, int parent_ifindex, gboolean force_check)
+{
+    NMDevicePrivate *priv;
+    NMDevice *       parent_device;
+    gboolean         changed = FALSE;
+    int              old_ifindex;
+    gs_unref_object NMDevice *old_device = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (parent_ifindex <= 0)
+        parent_ifindex = 0;
+
+    old_ifindex = priv->parent_ifindex;
+
+    if (priv->parent_ifindex == parent_ifindex) {
+        if (parent_ifindex > 0) {
+            if (!force_check && priv->parent_device.obj
+                && nm_device_get_ifindex(priv->parent_device.obj) == parent_ifindex)
+                return FALSE;
+        } else {
+            if (!priv->parent_device.obj)
+                return FALSE;
+        }
+    } else {
+        priv->parent_ifindex = parent_ifindex;
+        changed              = TRUE;
+    }
+
+    if (parent_ifindex > 0) {
+        parent_device = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, parent_ifindex);
+        if (parent_device == self)
+            parent_device = NULL;
+    } else
+        parent_device = NULL;
+
+    if (parent_device != priv->parent_device.obj) {
+        old_device = nm_g_object_ref(priv->parent_device.obj);
+        nm_dbus_track_obj_path_set(&priv->parent_device, parent_device, TRUE);
+        changed = TRUE;
+    }
+
+    if (changed) {
+        if (priv->parent_ifindex <= 0)
+            _LOGD(LOGD_DEVICE, "parent: clear");
+        else if (!priv->parent_device.obj)
+            _LOGD(LOGD_DEVICE, "parent: ifindex %d, no device", priv->parent_ifindex);
+        else {
+            _LOGD(LOGD_DEVICE,
+                  "parent: ifindex %d, device %p, %s",
+                  priv->parent_ifindex,
+                  priv->parent_device.obj,
+                  nm_device_get_iface(priv->parent_device.obj));
+        }
+
+        NM_DEVICE_GET_CLASS(self)->parent_changed_notify(self,
+                                                         old_ifindex,
+                                                         old_device,
+                                                         priv->parent_ifindex,
+                                                         priv->parent_device.obj);
+    }
+    return changed;
+}
+
+void
+nm_device_parent_set_ifindex(NMDevice *self, int parent_ifindex)
+{
+    _parent_set_ifindex(self, parent_ifindex, FALSE);
+}
+
+gboolean
+nm_device_parent_notify_changed(NMDevice *self, NMDevice *change_candidate, gboolean device_removed)
+{
+    NMDevicePrivate *priv;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_DEVICE(change_candidate));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->parent_ifindex > 0) {
+        if (priv->parent_device.obj == change_candidate
+            || priv->parent_ifindex == nm_device_get_ifindex(change_candidate))
+            return _parent_set_ifindex(self, priv->parent_ifindex, device_removed);
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+const char *
+nm_device_parent_find_for_connection(NMDevice *self, const char *current_setting_parent)
+{
+    const char *new_parent;
+    NMDevice *  parent_device;
+
+    parent_device = nm_device_parent_get_device(self);
+    if (!parent_device)
+        return NULL;
+
+    new_parent = nm_device_get_iface(parent_device);
+    if (!new_parent)
+        return NULL;
+
+    if (current_setting_parent && !nm_streq(current_setting_parent, new_parent)
+        && nm_utils_is_uuid(current_setting_parent)) {
+        NMSettingsConnection *parent_connection;
+
+        /* Don't change a parent specified by UUID if it's still valid */
+        parent_connection = nm_settings_get_connection_by_uuid(nm_device_get_settings(self),
+                                                               current_setting_parent);
+        if (parent_connection
+            && nm_device_check_connection_compatible(
+                parent_device,
+                nm_settings_connection_get_connection(parent_connection),
+                NULL))
+            return current_setting_parent;
+    }
+
+    return new_parent;
+}
+
+/*****************************************************************************/
+
+static void
+_stats_update_counters(NMDevice *self, guint64 tx_bytes, guint64 rx_bytes)
+{
+    NMDevicePrivate *priv;
+    gboolean         tx_changed = FALSE;
+    gboolean         rx_changed = FALSE;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->stats.tx_bytes != tx_bytes) {
+        priv->stats.tx_bytes = tx_bytes;
+        tx_changed           = TRUE;
+    }
+    if (priv->stats.rx_bytes != rx_bytes) {
+        priv->stats.rx_bytes = rx_bytes;
+        rx_changed           = TRUE;
+    }
+
+    nm_gobject_notify_together(self,
+                               tx_changed ? PROP_STATISTICS_TX_BYTES : PROP_0,
+                               rx_changed ? PROP_STATISTICS_RX_BYTES : PROP_0);
+}
+
+static void
+_stats_update_counters_from_pllink(NMDevice *self, const NMPlatformLink *pllink)
+{
+    _stats_update_counters(self, pllink->tx_bytes, pllink->rx_bytes);
+}
+
+static gboolean
+_stats_timeout_cb(gpointer user_data)
+{
+    NMDevice *self = user_data;
+    int       ifindex;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+
+    _LOGT(LOGD_DEVICE, "stats: refresh %d", ifindex);
+
+    if (ifindex > 0)
+        nm_platform_link_refresh(nm_device_get_platform(self), ifindex);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static guint
+_stats_refresh_rate_real(guint refresh_rate_ms)
+{
+    const guint STATS_REFRESH_RATE_MS_MIN = 200;
+
+    if (refresh_rate_ms == 0)
+        return 0;
+
+    if (refresh_rate_ms < STATS_REFRESH_RATE_MS_MIN) {
+        /* you cannot set the refresh-rate arbitrarily small. E.g.
+         * setting to 1ms is just killing. Have a lowest number. */
+        return STATS_REFRESH_RATE_MS_MIN;
+    }
+
+    return refresh_rate_ms;
+}
+
+static void
+_stats_set_refresh_rate(NMDevice *self, guint refresh_rate_ms)
+{
+    NMDevicePrivate *priv;
+    int              ifindex;
+    guint            old_rate;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->stats.refresh_rate_ms == refresh_rate_ms)
+        return;
+
+    old_rate                    = priv->stats.refresh_rate_ms;
+    priv->stats.refresh_rate_ms = refresh_rate_ms;
+    _notify(self, PROP_STATISTICS_REFRESH_RATE_MS);
+
+    _LOGD(LOGD_DEVICE, "stats: set refresh to %u ms", priv->stats.refresh_rate_ms);
+
+    if (!nm_device_is_real(self))
+        return;
+
+    refresh_rate_ms = _stats_refresh_rate_real(refresh_rate_ms);
+    if (_stats_refresh_rate_real(old_rate) == refresh_rate_ms)
+        return;
+
+    nm_clear_g_source(&priv->stats.timeout_id);
+
+    if (!refresh_rate_ms)
+        return;
+
+    /* trigger an initial refresh of the data whenever the refresh-rate changes.
+     * As we process the result in an idle handler with device_link_changed(),
+     * we don't get the result right away. */
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex > 0)
+        nm_platform_link_refresh(nm_device_get_platform(self), ifindex);
+
+    priv->stats.timeout_id = g_timeout_add(refresh_rate_ms, _stats_timeout_cb, self);
+}
+
+/*****************************************************************************/
+
+static gboolean
+get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *out_iid)
+{
+    NMDevicePrivate *     priv     = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform *          platform = nm_device_get_platform(self);
+    const NMPlatformLink *pllink;
+    const guint8 *        hwaddr;
+    guint8                pseudo_hwaddr[ETH_ALEN];
+    gsize                 hwaddr_len;
+    int                   ifindex;
+    gboolean              success;
+
+    /* If we get here, we *must* have a kernel netdev, which implies an ifindex */
+    ifindex = nm_device_get_ip_ifindex(self);
+    g_return_val_if_fail(ifindex > 0, FALSE);
+
+    pllink = nm_platform_link_get(platform, ifindex);
+    if (!pllink || NM_IN_SET(pllink->type, NM_LINK_TYPE_NONE, NM_LINK_TYPE_UNKNOWN))
+        return FALSE;
+
+    hwaddr = nmp_link_address_get(&pllink->l_address, &hwaddr_len);
+    if (hwaddr_len <= 0)
+        return FALSE;
+
+    if (pllink->type == NM_LINK_TYPE_6LOWPAN) {
+        /* If the underlying IEEE 802.15.4 device has a short address we generate
+         * a "pseudo 48-bit address" that's to be used in the same fashion as a
+         * wired Ethernet address. The mechanism is specified in Section 6. of
+         * RFC 4944 */
+        guint16 pan_id;
+        guint16 short_addr;
+
+        short_addr = nm_platform_wpan_get_short_addr(platform, pllink->parent);
+        if (short_addr != G_MAXUINT16) {
+            pan_id           = nm_platform_wpan_get_pan_id(platform, pllink->parent);
+            pseudo_hwaddr[0] = short_addr & 0xff;
+            pseudo_hwaddr[1] = (short_addr >> 8) & 0xff;
+            pseudo_hwaddr[2] = 0;
+            pseudo_hwaddr[3] = 0;
+            pseudo_hwaddr[4] = pan_id & 0xff;
+            pseudo_hwaddr[5] = (pan_id >> 8) & 0xff;
+
+            hwaddr     = pseudo_hwaddr;
+            hwaddr_len = G_N_ELEMENTS(pseudo_hwaddr);
+        }
+    }
+
+    success = nm_utils_get_ipv6_interface_identifier(pllink->type,
+                                                     hwaddr,
+                                                     hwaddr_len,
+                                                     priv->dev_id,
+                                                     out_iid);
+    if (!success) {
+        _LOGW(LOGD_PLATFORM,
+              "failed to generate interface identifier "
+              "for link type %u hwaddr_len %zu",
+              pllink->type,
+              hwaddr_len);
+    }
+    return success;
+}
+
+/**
+ * nm_device_get_ip_iface_identifier:
+ * @self: an #NMDevice
+ * @iid: where to place the interface identifier
+ * @ignore_token: force creation of a non-tokenized address
+ *
+ * Return the interface's identifier for the EUI64 address generation mode.
+ * It's either a manually set token or and identifier generated in a
+ * hardware-specific way.
+ *
+ * Unless @ignore_token is set the token is preferred. That is the case
+ * for link-local addresses (to mimic kernel behavior).
+ *
+ * Returns: #TRUE if the @iid could be set
+ */
+static gboolean
+nm_device_get_ip_iface_identifier(NMDevice *self, NMUtilsIPv6IfaceId *iid, gboolean ignore_token)
+{
+    NMSettingIP6Config *s_ip6;
+    const char *        token = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    if (!ignore_token) {
+        s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+
+        g_return_val_if_fail(s_ip6, FALSE);
+
+        token = nm_setting_ip6_config_get_token(s_ip6);
+    }
+    if (token)
+        return nm_utils_ipv6_interface_identifier_get_from_token(iid, token);
+    else
+        return NM_DEVICE_GET_CLASS(self)->get_ip_iface_identifier(self, iid);
+}
+
+const char *
+nm_device_get_driver(NMDevice *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->driver;
+}
+
+const char *
+nm_device_get_driver_version(NMDevice *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->driver_version;
+}
+
+NMDeviceType
+nm_device_get_device_type(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_TYPE_UNKNOWN);
+
+    return NM_DEVICE_GET_PRIVATE(self)->type;
+}
+
+NMLinkType
+nm_device_get_link_type(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_LINK_TYPE_UNKNOWN);
+
+    return NM_DEVICE_GET_PRIVATE(self)->link_type;
+}
+
+/**
+ * nm_device_get_metered:
+ * @setting: the #NMDevice
+ *
+ * Returns: the #NMDevice:metered property of the device.
+ *
+ * Since: 1.2
+ **/
+NMMetered
+nm_device_get_metered(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_METERED_UNKNOWN);
+
+    return NM_DEVICE_GET_PRIVATE(self)->metered;
+}
+
+guint32
+nm_device_get_route_metric_default(NMDeviceType device_type)
+{
+    /* Device 'priority' is used for the default route-metric and is based on
+     * the device type. The settings ipv4.route-metric and ipv6.route-metric
+     * can overwrite this default.
+     *
+     * For both IPv4 and IPv6 we use the same default values.
+     *
+     * The route-metric is used for the metric of the routes of device.
+     * This also applies to the default route. Therefore it affects also
+     * which device is the "best".
+     *
+     * For comparison, note that iproute2 by default adds IPv4 routes with
+     * metric 0, and IPv6 routes with metric 1024. The latter is the IPv6
+     * "user default" in the kernel (NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6).
+     * In kernel, the full uint32_t range is available for route
+     * metrics (except for IPv6, where 0 means 1024).
+     */
+
+    switch (device_type) {
+    /* 50 is also used for VPN plugins (NM_VPN_ROUTE_METRIC_DEFAULT).
+     *
+     * Note that returning 50 from this function means that this device-type is
+     * in some aspects a VPN. */
+    case NM_DEVICE_TYPE_WIREGUARD:
+        return NM_VPN_ROUTE_METRIC_DEFAULT;
+
+    case NM_DEVICE_TYPE_ETHERNET:
+    case NM_DEVICE_TYPE_VETH:
+        return 100;
+    case NM_DEVICE_TYPE_MACSEC:
+        return 125;
+    case NM_DEVICE_TYPE_INFINIBAND:
+        return 150;
+    case NM_DEVICE_TYPE_ADSL:
+        return 200;
+    case NM_DEVICE_TYPE_WIMAX:
+        return 250;
+    case NM_DEVICE_TYPE_BOND:
+        return 300;
+    case NM_DEVICE_TYPE_TEAM:
+        return 350;
+    case NM_DEVICE_TYPE_VLAN:
+        return 400;
+    case NM_DEVICE_TYPE_MACVLAN:
+        return 410;
+    case NM_DEVICE_TYPE_BRIDGE:
+        return 425;
+    case NM_DEVICE_TYPE_TUN:
+        return 450;
+    case NM_DEVICE_TYPE_PPP:
+        return 460;
+    case NM_DEVICE_TYPE_VRF:
+        return 470;
+    case NM_DEVICE_TYPE_VXLAN:
+        return 500;
+    case NM_DEVICE_TYPE_DUMMY:
+        return 550;
+    case NM_DEVICE_TYPE_WIFI:
+        return 600;
+    case NM_DEVICE_TYPE_OLPC_MESH:
+        return 650;
+    case NM_DEVICE_TYPE_IP_TUNNEL:
+        return 675;
+    case NM_DEVICE_TYPE_MODEM:
+        return 700;
+    case NM_DEVICE_TYPE_BT:
+        return 750;
+    case NM_DEVICE_TYPE_6LOWPAN:
+        return 775;
+    case NM_DEVICE_TYPE_OVS_BRIDGE:
+    case NM_DEVICE_TYPE_OVS_INTERFACE:
+    case NM_DEVICE_TYPE_OVS_PORT:
+        return 800;
+    case NM_DEVICE_TYPE_WPAN:
+        return 850;
+    case NM_DEVICE_TYPE_WIFI_P2P:
+    case NM_DEVICE_TYPE_GENERIC:
+        return 950;
+    case NM_DEVICE_TYPE_UNKNOWN:
+        return 10000;
+    case NM_DEVICE_TYPE_UNUSED1:
+    case NM_DEVICE_TYPE_UNUSED2:
+        /* omit default: to get compiler warning about missing switch cases */
+        break;
+    }
+    return 11000;
+}
+
+static gboolean
+default_route_metric_penalty_detect(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    /* currently we don't differentiate between IPv4 and IPv6 when detecting
+     * connectivity. */
+    if (priv->concheck_x[IS_IPv4].state != NM_CONNECTIVITY_FULL
+        && nm_connectivity_check_enabled(concheck_get_mgr(self)))
+        return TRUE;
+
+    return FALSE;
+}
+
+static guint32
+default_route_metric_penalty_get(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (NM_IS_IPv4(addr_family) ? priv->default_route_metric_penalty_ip4_has
+                                : priv->default_route_metric_penalty_ip6_has)
+        return 20000;
+    return 0;
+}
+
+guint32
+nm_device_get_route_metric(NMDevice *self, int addr_family)
+{
+    gint64             route_metric;
+    NMSettingIPConfig *s_ip;
+    NMConnection *     connection;
+    const char *       property;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_MAXUINT32);
+    g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), G_MAXUINT32);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+
+        /* Slave interfaces don't have IP settings, but we may get here when
+         * external changes are made or when noticing IP changes when starting
+         * the slave connection.
+         */
+        if (s_ip) {
+            route_metric = nm_setting_ip_config_get_route_metric(s_ip);
+            if (route_metric >= 0)
+                goto out;
+        }
+    }
+
+    /* use the current NMConfigData, which makes this configuration reloadable.
+     * Note that that means that the route-metric might change between SIGHUP.
+     * You must cache the returned value if that is a problem. */
+    property     = NM_IS_IPv4(addr_family) ? NM_CON_DEFAULT("ipv4.route-metric")
+                                           : NM_CON_DEFAULT("ipv6.route-metric");
+    route_metric = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                               property,
+                                                               self,
+                                                               0,
+                                                               G_MAXUINT32,
+                                                               -1);
+    if (route_metric >= 0)
+        goto out;
+
+    route_metric = nm_manager_device_route_metric_reserve(NM_MANAGER_GET,
+                                                          nm_device_get_ip_ifindex(self),
+                                                          nm_device_get_device_type(self));
+out:
+    return nm_utils_ip_route_metric_normalize(addr_family, route_metric);
+}
+
+guint32
+nm_device_get_route_table(NMDevice *self, int addr_family)
+{
+    guint32 route_table;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), RT_TABLE_MAIN);
+
+    route_table = _prop_get_ipvx_route_table(self, addr_family);
+    return route_table ?: (guint32) RT_TABLE_MAIN;
+}
+
+static NMIPRouteTableSyncMode
+_get_route_table_sync_mode_stateful(NMDevice *self, int addr_family)
+{
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMDedupMultiIter ipconf_iter;
+    gboolean         all_sync_now;
+    gboolean         all_sync_eff;
+
+    all_sync_now = _prop_get_ipvx_route_table(self, addr_family) != 0u;
+
+    if (!all_sync_now) {
+        const NMPlatformIPRoute *route;
+
+        /* If there's a local route switch to all-sync in order
+         * to properly manage the local table */
+        nm_ip_config_iter_ip_route_for_each (&ipconf_iter, priv->con_ip_config_x[IS_IPv4], &route) {
+            if (nm_platform_route_type_uncoerce(route->type_coerced) == RTN_LOCAL) {
+                all_sync_now = TRUE;
+                break;
+            }
+        }
+    }
+
+    if (all_sync_now)
+        all_sync_eff = TRUE;
+    else {
+        /* When we change from all-sync to no all-sync, we do a last all-sync one
+         * more time. For that, we determine the effective all-state based on the
+         * cached/previous all-sync flag.
+         *
+         * The purpose of this is to support reapply of route-table (and thus the
+         * all-sync mode). If reapply toggles from all-sync to no-all-sync, we must
+         * sync one last time. */
+        if (NM_IS_IPv4(addr_family))
+            all_sync_eff = priv->v4_route_table_all_sync_before;
+        else
+            all_sync_eff = priv->v6_route_table_all_sync_before;
+    }
+
+    if (NM_IS_IPv4(addr_family))
+        priv->v4_route_table_all_sync_before = all_sync_now;
+    else
+        priv->v6_route_table_all_sync_before = all_sync_now;
+
+    return all_sync_eff ? NM_IP_ROUTE_TABLE_SYNC_MODE_ALL : NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN;
+}
+
+const NMPObject *
+nm_device_get_best_default_route(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    switch (addr_family) {
+    case AF_INET:
+        return priv->ip_config_4 ? nm_ip4_config_best_default_route_get(priv->ip_config_4) : NULL;
+    case AF_INET6:
+        return priv->ip_config_6 ? nm_ip6_config_best_default_route_get(priv->ip_config_6) : NULL;
+    case AF_UNSPEC:
+        return (priv->ip_config_4 ? nm_ip4_config_best_default_route_get(priv->ip_config_4) : NULL)
+                   ?: (priv->ip_config_6 ? nm_ip6_config_best_default_route_get(priv->ip_config_6)
+                                         : NULL);
+    default:
+        g_return_val_if_reached(NULL);
+    }
+}
+
+const char *
+nm_device_get_type_desc(NMDevice *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->type_desc;
+}
+
+const char *
+nm_device_get_type_description(NMDevice *self)
+{
+    g_return_val_if_fail(self != NULL, NULL);
+
+    /* Beware: this function should return the same
+     * value as nm_device_get_type_description() in libnm. */
+
+    return NM_DEVICE_GET_CLASS(self)->get_type_description(self);
+}
+
+static const char *
+get_type_description(NMDevice *self)
+{
+    NMDeviceClass *klass;
+
+    nm_assert(NM_IS_DEVICE(self));
+
+    /* the default implementation for the description just returns the (modified)
+     * class name and depends entirely on the type of self. Note that we cache the
+     * description in the klass itself.
+     *
+     * Also note, that as the GObject class gets inited, it inherrits the fields
+     * of the parent class. That means, if NMDeviceVethClass was initialized after
+     * NMDeviceEthernetClass already has the description cached in the class
+     * (because we already fetched the description for an ethernet device),
+     * then default_type_description will wrongly contain "ethernet".
+     * To avoid that, and catch the situation, also cache the klass for
+     * which the description was cached. If that doesn't match, it was
+     * inherited and we need to reset it. */
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (G_UNLIKELY(klass->default_type_description_klass != klass)) {
+        const char *typename;
+        gs_free char *s = NULL;
+
+        typename = G_OBJECT_TYPE_NAME(self);
+        if (g_str_has_prefix(typename, "NMDevice")) {
+            typename += 8;
+            if (nm_streq(typename, "Veth"))
+                typename = "Ethernet";
+        }
+        s                                     = g_ascii_strdown(typename, -1);
+        klass->default_type_description       = g_intern_string(s);
+        klass->default_type_description_klass = klass;
+    }
+
+    nm_assert(klass->default_type_description);
+    return klass->default_type_description;
+}
+
+gboolean
+nm_device_has_carrier(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->carrier;
+}
+
+NMActRequest *
+nm_device_get_act_request(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->act_request.obj;
+}
+
+NMActivationStateFlags
+nm_device_get_activation_state_flags(NMDevice *self)
+{
+    NMActRequest *ac;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_ACTIVATION_STATE_FLAG_NONE);
+
+    ac = NM_DEVICE_GET_PRIVATE(self)->act_request.obj;
+    if (!ac)
+        return NM_ACTIVATION_STATE_FLAG_NONE;
+    return nm_active_connection_get_state_flags(NM_ACTIVE_CONNECTION(ac));
+}
+
+NMSettingsConnection *
+nm_device_get_settings_connection(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    return priv->act_request.obj ? nm_act_request_get_settings_connection(priv->act_request.obj)
+                                 : NULL;
+}
+
+NMConnection *
+nm_device_get_settings_connection_get_connection(NMDevice *self)
+{
+    NMSettingsConnection *sett_con;
+    NMDevicePrivate *     priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->act_request.obj)
+        return NULL;
+
+    sett_con = nm_act_request_get_settings_connection(priv->act_request.obj);
+    if (!sett_con)
+        return NULL;
+
+    return nm_settings_connection_get_connection(sett_con);
+}
+
+NMConnection *
+nm_device_get_applied_connection(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    return priv->act_request.obj ? nm_act_request_get_applied_connection(priv->act_request.obj)
+                                 : NULL;
+}
+
+gboolean
+nm_device_has_unmodified_applied_connection(NMDevice *self, NMSettingCompareFlags compare_flags)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->act_request.obj)
+        return FALSE;
+
+    return nm_active_connection_has_unmodified_applied_connection(
+        (NMActiveConnection *) priv->act_request.obj,
+        compare_flags);
+}
+
+gpointer
+nm_device_get_applied_setting(NMDevice *self, GType setting_type)
+{
+    NMConnection *connection;
+
+    connection = nm_device_get_applied_connection(self);
+    return connection ? nm_connection_get_setting(connection, setting_type) : NULL;
+}
+
+RfKillType
+nm_device_get_rfkill_type(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    return NM_DEVICE_GET_PRIVATE(self)->rfkill_type;
+}
+
+static const char *
+nm_device_get_physical_port_id(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->physical_port_id;
+}
+
+/*****************************************************************************/
+
+typedef enum {
+    CONCHECK_SCHEDULE_UPDATE_INTERVAL,
+    CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART,
+    CONCHECK_SCHEDULE_CHECK_EXTERNAL,
+    CONCHECK_SCHEDULE_CHECK_PERIODIC,
+    CONCHECK_SCHEDULE_RETURNED_MIN,
+    CONCHECK_SCHEDULE_RETURNED_BUMP,
+    CONCHECK_SCHEDULE_RETURNED_MAX,
+} ConcheckScheduleMode;
+
+static NMDeviceConnectivityHandle *concheck_start(NMDevice *                   self,
+                                                  int                          addr_family,
+                                                  NMDeviceConnectivityCallback callback,
+                                                  gpointer                     user_data,
+                                                  gboolean                     is_periodic);
+
+static void
+concheck_periodic_schedule_set(NMDevice *self, int addr_family, ConcheckScheduleMode mode);
+
+static gboolean
+_concheck_periodic_timeout_cb(NMDevice *self, int addr_family)
+{
+    _LOGt(LOGD_CONCHECK,
+          "connectivity: [IPv%c] periodic timeout",
+          nm_utils_addr_family_to_char(addr_family));
+    concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_CHECK_PERIODIC);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+concheck_ip4_periodic_timeout_cb(gpointer user_data)
+{
+    return _concheck_periodic_timeout_cb(user_data, AF_INET);
+}
+
+static gboolean
+concheck_ip6_periodic_timeout_cb(gpointer user_data)
+{
+    return _concheck_periodic_timeout_cb(user_data, AF_INET6);
+}
+
+static gboolean
+concheck_is_possible(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!nm_device_is_real(self) || is_loopback(self))
+        return FALSE;
+
+    /* we enable periodic checks for every device state (except UNKNOWN). Especially with
+     * unmanaged devices, it is interesting to know whether we have connectivity on that device. */
+    if (priv->state == NM_DEVICE_STATE_UNKNOWN)
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+concheck_periodic_schedule_do(NMDevice *self, int addr_family, gint64 now_ns)
+{
+    NMDevicePrivate *priv                    = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         periodic_check_disabled = FALSE;
+    gint64           expiry, tdiff;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    /* we always cancel whatever was pending. */
+    if (nm_clear_g_source(&priv->concheck_x[IS_IPv4].p_cur_id))
+        periodic_check_disabled = TRUE;
+
+    if (priv->concheck_x[IS_IPv4].p_max_interval == 0) {
+        /* periodic checks are disabled */
+        goto out;
+    }
+
+    if (!concheck_is_possible(self))
+        goto out;
+
+    nm_assert(now_ns > 0);
+    nm_assert(priv->concheck_x[IS_IPv4].p_cur_interval > 0);
+
+    /* we schedule the timeout based on our current settings cur-interval and cur-basetime.
+     * Before calling concheck_periodic_schedule_do(), make sure that these properties are
+     * correct. */
+
+    expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns
+             + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC);
+    tdiff = expiry - now_ns;
+
+    _LOGT(LOGD_CONCHECK,
+          "connectivity: [IPv%c] periodic-check: %sscheduled in %lld milliseconds (%u seconds "
+          "interval)",
+          nm_utils_addr_family_to_char(addr_family),
+          periodic_check_disabled ? "re-" : "",
+          (long long) (tdiff / NM_UTILS_NSEC_PER_MSEC),
+          priv->concheck_x[IS_IPv4].p_cur_interval);
+
+    priv->concheck_x[IS_IPv4].p_cur_id =
+        g_timeout_add(NM_MAX((gint64) 0, tdiff) / NM_UTILS_NSEC_PER_MSEC,
+                      IS_IPv4 ? concheck_ip4_periodic_timeout_cb : concheck_ip6_periodic_timeout_cb,
+                      self);
+    return TRUE;
+out:
+    if (periodic_check_disabled) {
+        _LOGT(LOGD_CONCHECK,
+              "connectivity: [IPv%c] periodic-check: unscheduled",
+              nm_utils_addr_family_to_char(addr_family));
+    }
+    return FALSE;
+}
+
+#define CONCHECK_P_PROBE_INTERVAL 1
+
+static void
+concheck_periodic_schedule_set(NMDevice *self, int addr_family, ConcheckScheduleMode mode)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gint64           new_expiry, exp_expiry, cur_expiry, tdiff;
+    gint64           now_ns  = 0;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (priv->concheck_x[IS_IPv4].p_max_interval == 0) {
+        /* periodic check is disabled. Nothing to do. */
+        return;
+    }
+
+    if (!priv->concheck_x[IS_IPv4].p_cur_id) {
+        /* we currently don't have a timeout scheduled. No need to reschedule
+         * another one... */
+        if (NM_IN_SET(mode,
+                      CONCHECK_SCHEDULE_UPDATE_INTERVAL,
+                      CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART)) {
+            /* ... unless, we are about to start periodic checks after update-interval.
+             * In this case, fall through and restart the periodic checks below. */
+            mode = CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART;
+        } else
+            return;
+    }
+
+    switch (mode) {
+    case CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART:
+        priv->concheck_x[IS_IPv4].p_cur_interval =
+            NM_MIN(priv->concheck_x[IS_IPv4].p_max_interval, CONCHECK_P_PROBE_INTERVAL);
+        priv->concheck_x[IS_IPv4].p_cur_basetime_ns =
+            nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns);
+        if (concheck_periodic_schedule_do(self, addr_family, now_ns))
+            concheck_start(self, addr_family, NULL, NULL, TRUE);
+        return;
+
+    case CONCHECK_SCHEDULE_UPDATE_INTERVAL:
+        /* called with "UPDATE_INTERVAL" and already have a p_cur_id scheduled. */
+
+        nm_assert(priv->concheck_x[IS_IPv4].p_max_interval > 0);
+        nm_assert(priv->concheck_x[IS_IPv4].p_cur_interval > 0);
+
+        if (priv->concheck_x[IS_IPv4].p_cur_interval <= priv->concheck_x[IS_IPv4].p_max_interval) {
+            /* we currently have a shorter interval set, than what we now have. Either,
+             * because we are probing, or because the previous max interval was shorter.
+             *
+             * Either way, the current timer is set just fine. Nothing to do, we will
+             * probe our way up. */
+            return;
+        }
+
+        cur_expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns
+                     + (priv->concheck_x[IS_IPv4].p_max_interval * NM_UTILS_NSEC_PER_SEC);
+        nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns);
+
+        priv->concheck_x[IS_IPv4].p_cur_interval = priv->concheck_x[IS_IPv4].p_max_interval;
+        if (cur_expiry <= now_ns) {
+            /* Since the last time we scheduled a periodic check, already more than the
+             * new max_interval passed. We need to start a check right away (and
+             * schedule a timeout in cur-interval in the future). */
+            priv->concheck_x[IS_IPv4].p_cur_basetime_ns = now_ns;
+            if (concheck_periodic_schedule_do(self, addr_family, now_ns))
+                concheck_start(self, addr_family, NULL, NULL, TRUE);
+        } else {
+            /* we are reducing the max-interval to a shorter interval that we have currently
+             * scheduled (with cur_interval).
+             *
+             * However, since the last time we scheduled the check, not even the new max-interval
+             * expired. All we need to do, is reschedule the timer to expire sooner. The cur_basetime
+             * is unchanged. */
+            concheck_periodic_schedule_do(self, addr_family, now_ns);
+        }
+        return;
+
+    case CONCHECK_SCHEDULE_CHECK_EXTERNAL:
+        /* a external connectivity check delays our periodic check. We reset the counter. */
+        priv->concheck_x[IS_IPv4].p_cur_basetime_ns =
+            nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns);
+        concheck_periodic_schedule_do(self, addr_family, now_ns);
+        return;
+
+    case CONCHECK_SCHEDULE_CHECK_PERIODIC:
+    {
+        gboolean                    any_periodic_pending;
+        NMDeviceConnectivityHandle *handle;
+        guint                       old_interval = priv->concheck_x[IS_IPv4].p_cur_interval;
+
+        any_periodic_pending = FALSE;
+        c_list_for_each_entry (handle, &priv->concheck_lst_head, concheck_lst) {
+            if (handle->addr_family != addr_family)
+                continue;
+            if (handle->is_periodic_bump) {
+                handle->is_periodic_bump             = FALSE;
+                handle->is_periodic_bump_on_complete = FALSE;
+                any_periodic_pending                 = TRUE;
+            }
+        }
+        if (any_periodic_pending) {
+            /* we reached a timeout to schedule a new periodic request, however we still
+             * have period requests pending that didn't complete yet. We need to bump the
+             * interval already. */
+            priv->concheck_x[IS_IPv4].p_cur_interval =
+                NM_MIN(old_interval * 2, priv->concheck_x[IS_IPv4].p_max_interval);
+        }
+
+        /* we just reached a timeout. The expected expiry (exp_expiry) should be
+         * pretty close to now_ns.
+         *
+         * We want to reschedule the timeout at exp_expiry (aka now) + cur_interval. */
+        nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns);
+        exp_expiry =
+            priv->concheck_x[IS_IPv4].p_cur_basetime_ns + (old_interval * NM_UTILS_NSEC_PER_SEC);
+        new_expiry =
+            exp_expiry + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC);
+        tdiff = NM_MAX(new_expiry - now_ns, 0);
+        priv->concheck_x[IS_IPv4].p_cur_basetime_ns =
+            (now_ns + tdiff) - (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC);
+        if (concheck_periodic_schedule_do(self, addr_family, now_ns)) {
+            handle = concheck_start(self, addr_family, NULL, NULL, TRUE);
+            if (old_interval != priv->concheck_x[IS_IPv4].p_cur_interval) {
+                /* we just bumped the interval already when scheduling this check.
+                 * When the handle returns, don't bump a second time.
+                 *
+                 * But if we reach the timeout again before the handle returns (this
+                 * code here) we will still bump the interval. */
+                handle->is_periodic_bump_on_complete = FALSE;
+            }
+        }
+        return;
+    }
+
+    /* we just got an event that we lost connectivity (that is, concheck returned). We reset
+     * the interval to min/max or increase the probe interval (bump). */
+    case CONCHECK_SCHEDULE_RETURNED_MIN:
+        priv->concheck_x[IS_IPv4].p_cur_interval =
+            NM_MIN(priv->concheck_x[IS_IPv4].p_max_interval, CONCHECK_P_PROBE_INTERVAL);
+        break;
+    case CONCHECK_SCHEDULE_RETURNED_MAX:
+        priv->concheck_x[IS_IPv4].p_cur_interval = priv->concheck_x[IS_IPv4].p_max_interval;
+        break;
+    case CONCHECK_SCHEDULE_RETURNED_BUMP:
+        priv->concheck_x[IS_IPv4].p_cur_interval =
+            NM_MIN(priv->concheck_x[IS_IPv4].p_cur_interval * 2,
+                   priv->concheck_x[IS_IPv4].p_max_interval);
+        break;
+    }
+
+    /* we are here, because we returned from a connectivity check and adjust the current interval.
+     *
+     * But note that we calculate the new timeout based on the time when we scheduled the
+     * last check, instead of counting from now. The reason is that we want that the times
+     * when we schedule checks be at precise intervals, without including the time it took for
+     * the connectivity check. */
+    new_expiry = priv->concheck_x[IS_IPv4].p_cur_basetime_ns
+                 + (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC);
+    tdiff = NM_MAX(new_expiry - nm_utils_get_monotonic_timestamp_nsec_cached(&now_ns), 0);
+    priv->concheck_x[IS_IPv4].p_cur_basetime_ns =
+        now_ns + tdiff - (priv->concheck_x[IS_IPv4].p_cur_interval * NM_UTILS_NSEC_PER_SEC);
+    concheck_periodic_schedule_do(self, addr_family, now_ns);
+}
+
+static void
+concheck_update_interval(NMDevice *self, int addr_family, gboolean check_now)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    guint            new_interval;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    new_interval = nm_connectivity_get_interval(concheck_get_mgr(self));
+
+    new_interval = NM_MIN(new_interval, 7 * 24 * 3600);
+
+    if (new_interval != priv->concheck_x[IS_IPv4].p_max_interval) {
+        _LOGT(LOGD_CONCHECK,
+              "connectivity: [IPv%c] periodic-check: set interval to %u seconds",
+              nm_utils_addr_family_to_char(addr_family),
+              new_interval);
+        priv->concheck_x[IS_IPv4].p_max_interval = new_interval;
+    }
+
+    if (!new_interval) {
+        /* this will cancel any potentially pending timeout because max-interval is zero.
+         * But it logs a nice message... */
+        concheck_periodic_schedule_do(self, addr_family, 0);
+
+        /* also update the fake connectivity state. */
+        concheck_update_state(self, addr_family, NM_CONNECTIVITY_FAKE, TRUE);
+        return;
+    }
+
+    concheck_periodic_schedule_set(self,
+                                   addr_family,
+                                   check_now ? CONCHECK_SCHEDULE_UPDATE_INTERVAL_RESTART
+                                             : CONCHECK_SCHEDULE_UPDATE_INTERVAL);
+}
+
+void
+nm_device_check_connectivity_update_interval(NMDevice *self)
+{
+    concheck_update_interval(self, AF_INET, TRUE);
+    concheck_update_interval(self, AF_INET6, TRUE);
+}
+
+static void
+concheck_update_state(NMDevice *          self,
+                      int                 addr_family,
+                      NMConnectivityState state,
+                      gboolean            allow_periodic_bump)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    /* @state is a result of the connectivity check. We only expect a precise
+     * number of possible values. */
+    nm_assert(NM_IN_SET(state,
+                        NM_CONNECTIVITY_LIMITED,
+                        NM_CONNECTIVITY_PORTAL,
+                        NM_CONNECTIVITY_FULL,
+                        NM_CONNECTIVITY_FAKE,
+                        NM_CONNECTIVITY_NONE,
+                        NM_CONNECTIVITY_ERROR));
+
+    if (state == NM_CONNECTIVITY_ERROR) {
+        /* on error, we don't change the current connectivity state,
+         * except making UNKNOWN to NONE. */
+        state = priv->concheck_x[IS_IPv4].state;
+        if (state == NM_CONNECTIVITY_UNKNOWN)
+            state = NM_CONNECTIVITY_NONE;
+    } else if (state == NM_CONNECTIVITY_FAKE) {
+        /* If the connectivity check is disabled and we obtain a fake
+         * result, make an optimistic guess. */
+        if (priv->state == NM_DEVICE_STATE_ACTIVATED) {
+            /* FIXME: the fake connectivity state depends on the availability of
+             * a default route. However, we have no mechanism that rechecks the
+             * value if a device route appears/disappears after the device
+             * was activated. */
+            if (nm_device_get_best_default_route(self, AF_UNSPEC))
+                state = NM_CONNECTIVITY_FULL;
+            else
+                state = NM_CONNECTIVITY_LIMITED;
+        } else
+            state = NM_CONNECTIVITY_NONE;
+    }
+
+    if (priv->concheck_x[IS_IPv4].state == state) {
+        /* we got a connectivity update, but the state didn't change. If we were probing,
+         * we bump the probe frequency. */
+        if (allow_periodic_bump)
+            concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_BUMP);
+        return;
+    }
+    /* we need to update the probe interval before emitting signals. Emitting
+     * a signal might call back into NMDevice and change the probe settings.
+     * So, do that first. */
+    if (state == NM_CONNECTIVITY_FULL) {
+        /* we reached full connectivity state. Stop probing by setting the
+         * interval to the max. */
+        concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_MAX);
+    } else if (priv->concheck_x[IS_IPv4].state == NM_CONNECTIVITY_FULL) {
+        /* we are about to loose connectivity. (re)start probing by setting
+         * the timeout interval to the min. */
+        concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_MIN);
+    } else {
+        if (allow_periodic_bump)
+            concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_RETURNED_BUMP);
+    }
+
+    _LOGD(LOGD_CONCHECK,
+          "connectivity state changed from %s to %s",
+          nm_connectivity_state_to_string(priv->concheck_x[IS_IPv4].state),
+          nm_connectivity_state_to_string(state));
+    priv->concheck_x[IS_IPv4].state = state;
+
+    _notify(self, IS_IPv4 ? PROP_IP4_CONNECTIVITY : PROP_IP6_CONNECTIVITY);
+
+    if (priv->state == NM_DEVICE_STATE_ACTIVATED && !nm_device_sys_iface_state_is_external(self)) {
+        if (nm_device_get_best_default_route(self, AF_INET)
+            && !ip_config_merge_and_apply(self, AF_INET, TRUE))
+            _LOGW(LOGD_IP4, "Failed to update IPv4 route metric");
+        if (nm_device_get_best_default_route(self, AF_INET6)
+            && !ip_config_merge_and_apply(self, AF_INET6, TRUE))
+            _LOGW(LOGD_IP6, "Failed to update IPv6 route metric");
+    }
+}
+
+static const char *
+nm_device_get_effective_ip_config_method(NMDevice *self, int addr_family)
+{
+    NMDeviceClass *klass;
+    NMConnection * connection = nm_device_get_applied_connection(self);
+    const char *   method;
+    const int      IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), "" /* bogus */);
+
+    method = nm_utils_get_ip_config_method(connection, addr_family);
+
+    if ((IS_IPv4 && nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO))
+        || (!IS_IPv4 && nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO))) {
+        klass = NM_DEVICE_GET_CLASS(self);
+        if (klass->get_auto_ip_config_method) {
+            const char *auto_method;
+
+            auto_method = klass->get_auto_ip_config_method(self, addr_family);
+            if (auto_method)
+                return auto_method;
+        }
+    }
+
+    return method;
+}
+
+static void
+concheck_handle_complete(NMDeviceConnectivityHandle *handle, GError *error)
+{
+    const int IS_IPv4 = NM_IS_IPv4(handle->addr_family);
+
+    /* The moment we invoke the callback, we unlink it. It signals
+     * that @handle is handled -- as far as the callee of callback
+     * is concerned. */
+    c_list_unlink(&handle->concheck_lst);
+
+    if (handle->c_handle)
+        nm_connectivity_check_cancel(handle->c_handle);
+
+    if (handle->callback) {
+        handle->callback(handle->self,
+                         handle,
+                         NM_DEVICE_GET_PRIVATE(handle->self)->concheck_x[IS_IPv4].state,
+                         error,
+                         handle->user_data);
+    }
+
+    g_slice_free(NMDeviceConnectivityHandle, handle);
+}
+
+static void
+concheck_cb(NMConnectivity *           connectivity,
+            NMConnectivityCheckHandle *c_handle,
+            NMConnectivityState        state,
+            gpointer                   user_data)
+{
+    _nm_unused gs_unref_object NMDevice *self_keep_alive = NULL;
+    NMDevice *                           self;
+    NMDevicePrivate *                    priv;
+    NMDeviceConnectivityHandle *         handle;
+    NMDeviceConnectivityHandle *         other_handle;
+    gboolean                             handle_is_alive;
+    gboolean                             allow_periodic_bump;
+    gboolean                             any_periodic_before;
+    gboolean                             any_periodic_after;
+    guint64                              seq;
+
+    handle = user_data;
+    nm_assert(handle->c_handle == c_handle);
+    nm_assert(NM_IS_DEVICE(handle->self));
+
+    handle->c_handle = NULL;
+    self             = handle->self;
+
+    if (state == NM_CONNECTIVITY_CANCELLED) {
+        /* the only place where we nm_connectivity_check_cancel(@c_handle), is
+         * from inside concheck_handle_complete(). This is a recursive call,
+         * nothing to do. */
+        _LOGT(LOGD_CONCHECK,
+              "connectivity: [IPv%c] complete check (seq:%llu, cancelled)",
+              nm_utils_addr_family_to_char(handle->addr_family),
+              (long long unsigned) handle->seq);
+        return;
+    }
+
+    /* we keep NMConnectivity instance alive. It cannot be disposing. */
+    nm_assert(state != NM_CONNECTIVITY_DISPOSING);
+
+    self_keep_alive = g_object_ref(self);
+
+    /* keep @self alive, while we invoke callbacks. */
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(handle && c_list_contains(&priv->concheck_lst_head, &handle->concheck_lst));
+
+    seq = handle->seq;
+
+    _LOGT(LOGD_CONCHECK,
+          "connectivity: [Ipv%c] complete check (seq:%llu, state:%s)",
+          nm_utils_addr_family_to_char(handle->addr_family),
+          (long long unsigned) handle->seq,
+          nm_connectivity_state_to_string(state));
+
+    /* find out, if there are any periodic checks pending (either whether they
+     * were scheduled before or after @handle. */
+    any_periodic_before = FALSE;
+    any_periodic_after  = FALSE;
+    c_list_for_each_entry (other_handle, &priv->concheck_lst_head, concheck_lst) {
+        if (other_handle->addr_family != handle->addr_family)
+            continue;
+        if (other_handle->is_periodic_bump_on_complete) {
+            if (other_handle->seq < seq)
+                any_periodic_before = TRUE;
+            else if (other_handle->seq > seq)
+                any_periodic_after = TRUE;
+        }
+    }
+    if (NM_IN_SET(state, NM_CONNECTIVITY_ERROR)) {
+        /* the request failed. We consider this periodic check only as completed if
+         * this was a periodic check, and there are not checks pending (either
+         * before or after this one).
+         *
+         * We allow_periodic_bump, if the request failed and there are
+         * still other requests periodic pending. */
+        allow_periodic_bump =
+            handle->is_periodic_bump_on_complete && !any_periodic_before && !any_periodic_after;
+    } else {
+        /* the request succeeded. This marks the completion of a periodic check,
+         * if this handle was periodic, or any previously scheduled one (that
+         * we are going to complete below). */
+        allow_periodic_bump = handle->is_periodic_bump_on_complete || any_periodic_before;
+    }
+
+    /* first update the new state, and emit signals. */
+    concheck_update_state(self, handle->addr_family, state, allow_periodic_bump);
+
+    handle_is_alive = FALSE;
+
+    /* we might have invoked callbacks during concheck_update_state(). The caller might have
+     * cancelled and thus destroyed @handle. We have to check whether handle is still alive,
+     * by searching it in the list of alive handles.
+     *
+     * Also, we might want to complete all pending callbacks that were started before
+     * @handle, as they are automatically obsoleted. */
+check_handles:
+    c_list_for_each_entry (other_handle, &priv->concheck_lst_head, concheck_lst) {
+        if (other_handle->addr_family != handle->addr_family)
+            continue;
+        if (other_handle->seq >= seq) {
+            /* it's not guaranteed that @handle is still in the list. It might already
+             * be canceled while invoking callbacks for a previous other_handle.
+             * If it is already cancelled, @handle is a dangling pointer.
+             *
+             * Since @seq is assigned uniquely and increasing, either @other_handle is
+             * @handle (and thus, handle is alive), or it isn't. */
+            if (other_handle == handle)
+                handle_is_alive = TRUE;
+            break;
+        }
+
+        nm_assert(other_handle != handle);
+
+        if (!NM_IN_SET(state, NM_CONNECTIVITY_ERROR)) {
+            /* we also want to complete handles that were started before the current
+             * @handle. Their response is out-dated. */
+            concheck_handle_complete(other_handle, NULL);
+
+            /* we invoked callbacks, other handles might be cancelled and removed from the list.
+             * Need to iterate the list from the start. */
+            goto check_handles;
+        }
+    }
+
+    if (!handle_is_alive) {
+        /* We didn't find @handle in the list of alive handles. Thus, the handles
+         * was cancelled while we were invoking events. Nothing to do, and don't
+         * touch the dangling pointer. */
+        return;
+    }
+
+    concheck_handle_complete(handle, NULL);
+}
+
+static NMDeviceConnectivityHandle *
+concheck_start(NMDevice *                   self,
+               int                          addr_family,
+               NMDeviceConnectivityCallback callback,
+               gpointer                     user_data,
+               gboolean                     is_periodic)
+{
+    static guint64              seq_counter = 0;
+    NMDevicePrivate *           priv;
+    NMDeviceConnectivityHandle *handle;
+    const char *                ifname;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    handle                               = g_slice_new0(NMDeviceConnectivityHandle);
+    handle->seq                          = ++seq_counter;
+    handle->self                         = self;
+    handle->callback                     = callback;
+    handle->user_data                    = user_data;
+    handle->is_periodic                  = is_periodic;
+    handle->is_periodic_bump             = is_periodic;
+    handle->is_periodic_bump_on_complete = is_periodic;
+    handle->addr_family                  = addr_family;
+
+    c_list_link_tail(&priv->concheck_lst_head, &handle->concheck_lst);
+
+    _LOGT(LOGD_CONCHECK,
+          "connectivity: [IPv%c] start check (seq:%llu%s)",
+          nm_utils_addr_family_to_char(addr_family),
+          (long long unsigned) handle->seq,
+          is_periodic ? ", periodic-check" : "");
+
+    if (NM_IS_IPv4(addr_family) && !priv->concheck_rp_filter_checked) {
+        if ((ifname = nm_device_get_ip_iface_from_platform(self))) {
+            gboolean due_to_all;
+            int      val;
+
+            val = nm_platform_sysctl_ip_conf_get_rp_filter_ipv4(nm_device_get_platform(self),
+                                                                ifname,
+                                                                TRUE,
+                                                                &due_to_all);
+            if (val == 1) {
+                _LOGW(LOGD_CONCHECK,
+                      "connectivity: \"/proc/sys/net/ipv4/conf/%s/rp_filter\" is set to \"1\". "
+                      "This might break connectivity checking for IPv4 on this device",
+                      due_to_all ? "all" : ifname);
+            }
+        }
+
+        /* we only check once per device. It's a warning after all.  */
+        priv->concheck_rp_filter_checked = TRUE;
+    }
+
+    handle->c_handle = nm_connectivity_check_start(concheck_get_mgr(self),
+                                                   handle->addr_family,
+                                                   nm_device_get_platform(self),
+                                                   nm_device_get_ip_ifindex(self),
+                                                   nm_device_get_ip_iface(self),
+                                                   concheck_cb,
+                                                   handle);
+    return handle;
+}
+
+NMDeviceConnectivityHandle *
+nm_device_check_connectivity(NMDevice *                   self,
+                             int                          addr_family,
+                             NMDeviceConnectivityCallback callback,
+                             gpointer                     user_data)
+{
+    if (!concheck_is_possible(self))
+        return NULL;
+
+    concheck_periodic_schedule_set(self, addr_family, CONCHECK_SCHEDULE_CHECK_EXTERNAL);
+    return concheck_start(self, addr_family, callback, user_data, FALSE);
+}
+
+void
+nm_device_check_connectivity_cancel(NMDeviceConnectivityHandle *handle)
+{
+    gs_free_error GError *cancelled_error = NULL;
+
+    g_return_if_fail(handle);
+    g_return_if_fail(NM_IS_DEVICE(handle->self));
+    g_return_if_fail(!c_list_is_empty(&handle->concheck_lst));
+
+    /* nobody has access to periodic handles, and cannot cancel
+     * them externally. */
+    nm_assert(!handle->is_periodic);
+
+    nm_utils_error_set_cancelled(&cancelled_error, FALSE, "NMDevice");
+    concheck_handle_complete(handle, cancelled_error);
+}
+
+NMConnectivityState
+nm_device_get_connectivity_state(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_CONNECTIVITY_UNKNOWN);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    switch (addr_family) {
+    case AF_INET:
+    case AF_INET6:
+        return priv->concheck_x[NM_IS_IPv4(addr_family)].state;
+    default:
+        nm_assert(addr_family == AF_UNSPEC);
+        return NM_MAX_WITH_CMP(nm_connectivity_state_cmp,
+                               priv->concheck_x[0].state,
+                               priv->concheck_x[1].state);
+    }
+}
+
+/*****************************************************************************/
+
+static SlaveInfo *
+find_slave_info(NMDevice *self, NMDevice *slave)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    CList *          iter;
+    SlaveInfo *      info;
+
+    c_list_for_each (iter, &priv->slaves) {
+        info = c_list_entry(iter, SlaveInfo, lst_slave);
+        if (info->slave == slave)
+            return info;
+    }
+    return NULL;
+}
+
+/**
+ * nm_device_master_enslave_slave:
+ * @self: the master device
+ * @slave: the slave device to enslave
+ * @connection: (allow-none): the slave device's connection
+ *
+ * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
+ * etc) then this function enslaves @slave.
+ *
+ * Returns: %TRUE on success, %FALSE on failure or if this device cannot enslave
+ *  other devices.
+ */
+static gboolean
+nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *connection)
+{
+    NMDevicePrivate *priv;
+    SlaveInfo *      info;
+    gboolean         success = FALSE;
+    gboolean         configure;
+
+    g_return_val_if_fail(self != NULL, FALSE);
+    g_return_val_if_fail(slave != NULL, FALSE);
+    g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    info = find_slave_info(self, slave);
+    if (!info)
+        return FALSE;
+
+    if (info->slave_is_enslaved)
+        success = TRUE;
+    else {
+        configure = (info->configure && connection != NULL);
+        if (configure)
+            g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE);
+
+        success = NM_DEVICE_GET_CLASS(self)->enslave_slave(self, slave, connection, configure);
+        info->slave_is_enslaved = success;
+    }
+
+    nm_device_slave_notify_enslave(info->slave, success);
+
+    /* Ensure the device's hardware address is up-to-date; it often changes
+     * when slaves change.
+     */
+    nm_device_update_hw_address(self);
+
+    /* Send ARP announcements if did not yet and have addresses. */
+    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !priv->acd.announcing)
+        nm_device_arp_announce(self);
+
+    /* Restart IP configuration if we're waiting for slaves.  Do this
+     * after updating the hardware address as IP config may need the
+     * new address.
+     */
+    if (success) {
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_WAIT)
+            nm_device_activate_stage3_ip_start(self, AF_INET);
+
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_WAIT)
+            nm_device_activate_stage3_ip_start(self, AF_INET6);
+    }
+
+    /* Since slave devices don't have their own IP configuration,
+     * set the MTU here.
+     */
+    _commit_mtu(slave, NM_DEVICE_GET_PRIVATE(slave)->ip_config_4);
+
+    return success;
+}
+
+/**
+ * nm_device_master_release_one_slave:
+ * @self: the master device
+ * @slave: the slave device to release
+ * @configure: whether @self needs to actually release @slave
+ * @force: force the release of @slave even if it wasn't added
+ *         to @master by NetworkManager
+ * @reason: the state change reason for the @slave
+ *
+ * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
+ * etc) then this function releases the previously enslaved @slave and/or
+ * updates the state of @self and @slave to reflect its release.
+ */
+static void
+nm_device_master_release_one_slave(NMDevice *          self,
+                                   NMDevice *          slave,
+                                   gboolean            configure,
+                                   gboolean            force,
+                                   NMDeviceStateReason reason)
+{
+    NMDevicePrivate *priv;
+    NMDevicePrivate *slave_priv;
+    SlaveInfo *      info;
+    gs_unref_object NMDevice *self_free = NULL;
+
+    g_return_if_fail(NM_DEVICE(self));
+    g_return_if_fail(NM_DEVICE(slave));
+    g_return_if_fail(!force || configure);
+    g_return_if_fail(NM_DEVICE_GET_CLASS(self)->release_slave != NULL);
+
+    info = find_slave_info(self, slave);
+
+    _LOGT(LOGD_CORE,
+          "master: release one slave %p/%s %s%s",
+          slave,
+          nm_device_get_iface(slave),
+          !info ? "(not registered)" : (info->slave_is_enslaved ? "(enslaved)" : "(not enslaved)"),
+          force ? " (force-configure)" : (configure ? " (configure)" : ""));
+
+    if (!info)
+        g_return_if_reached();
+
+    priv       = NM_DEVICE_GET_PRIVATE(self);
+    slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+
+    g_return_if_fail(self == slave_priv->master);
+    nm_assert(slave == info->slave);
+
+    /* first, let subclasses handle the release ... */
+    if (info->slave_is_enslaved || nm_device_sys_iface_state_is_external(slave) || force)
+        NM_DEVICE_GET_CLASS(self)->release_slave(self, slave, configure);
+
+    /* raise notifications about the release, including clearing is_enslaved. */
+    nm_device_slave_notify_release(slave, reason);
+
+    /* keep both alive until the end of the function.
+     * Transfers ownership from slave_priv->master.  */
+    self_free = self;
+
+    c_list_unlink(&info->lst_slave);
+    slave_priv->master = NULL;
+
+    g_signal_handler_disconnect(slave, info->watch_id);
+    g_object_unref(slave);
+    g_slice_free(SlaveInfo, info);
+
+    if (c_list_is_empty(&priv->slaves)) {
+        _active_connection_set_state_flags_full(self,
+                                                0,
+                                                NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES);
+    }
+
+    /* Ensure the device's hardware address is up-to-date; it often changes
+     * when slaves change.
+     */
+    nm_device_update_hw_address(self);
+    nm_device_set_unmanaged_by_flags(slave,
+                                     NM_UNMANAGED_IS_SLAVE,
+                                     NM_UNMAN_FLAG_OP_FORGET,
+                                     NM_DEVICE_STATE_REASON_REMOVED);
+}
+
+/**
+ * can_unmanaged_external_down:
+ * @self: the device
+ *
+ * Check whether the device should stay NM_UNMANAGED_EXTERNAL_DOWN unless
+ * IFF_UP-ed externally.
+ */
+static gboolean
+can_unmanaged_external_down(NMDevice *self)
+{
+    return !NM_DEVICE_GET_PRIVATE(self)->nm_owned && nm_device_is_software(self);
+}
+
+static NMUnmanFlagOp
+is_unmanaged_external_down(NMDevice *self, gboolean consider_can)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (consider_can && !NM_DEVICE_GET_CLASS(self)->can_unmanaged_external_down(self))
+        return NM_UNMAN_FLAG_OP_FORGET;
+
+    /* Manage externally-created software interfaces only when they are IFF_UP */
+    if (priv->ifindex <= 0 || !priv->up
+        || !(!c_list_is_empty(&priv->slaves)
+             || nm_platform_link_can_assume(nm_device_get_platform(self), priv->ifindex)))
+        return NM_UNMAN_FLAG_OP_SET_UNMANAGED;
+
+    return NM_UNMAN_FLAG_OP_SET_MANAGED;
+}
+
+static void
+set_unmanaged_external_down(NMDevice *self, gboolean only_if_unmanaged)
+{
+    NMUnmanFlagOp ext_flags;
+
+    if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN))
+        return;
+
+    if (only_if_unmanaged) {
+        if (!nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN))
+            return;
+    }
+
+    ext_flags = is_unmanaged_external_down(self, FALSE);
+    if (ext_flags != NM_UNMAN_FLAG_OP_SET_UNMANAGED) {
+        /* Ensure the assume check is queued before any queued state changes
+         * from the transition to UNAVAILABLE.
+         */
+        nm_device_queue_recheck_assume(self);
+    }
+
+    nm_device_set_unmanaged_by_flags(self,
+                                     NM_UNMANAGED_EXTERNAL_DOWN,
+                                     ext_flags,
+                                     NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+}
+
+void
+nm_device_update_dynamic_ip_setup(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+    GError *         error = NULL;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->state < NM_DEVICE_STATE_IP_CONFIG || priv->state > NM_DEVICE_STATE_ACTIVATED)
+        return;
+
+    g_hash_table_remove_all(priv->ip6_saved_properties);
+
+    if (priv->dhcp_data_4.client) {
+        if (!nm_device_dhcp4_renew(self, FALSE)) {
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_DHCP_FAILED);
+            return;
+        }
+    }
+    if (priv->dhcp_data_6.client) {
+        if (!nm_device_dhcp6_renew(self, FALSE)) {
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_DHCP_FAILED);
+            return;
+        }
+    }
+    if (priv->ndisc) {
+        /* FIXME: todo */
+    }
+    if (priv->dnsmasq_manager) {
+        /* FIXME: todo */
+    }
+
+    if (priv->lldp_listener && nm_lldp_listener_is_running(priv->lldp_listener)) {
+        nm_lldp_listener_stop(priv->lldp_listener);
+        if (!nm_lldp_listener_start(priv->lldp_listener, nm_device_get_ifindex(self), &error)) {
+            _LOGD(LOGD_DEVICE,
+                  "LLDP listener %p could not be restarted: %s",
+                  priv->lldp_listener,
+                  error->message);
+            g_clear_error(&error);
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+carrier_changed_notify(NMDevice *self, gboolean carrier)
+{
+    /* stub */
+}
+
+static void
+carrier_changed(NMDevice *self, gboolean carrier)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->state <= NM_DEVICE_STATE_UNMANAGED)
+        return;
+
+    nm_device_recheck_available_connections(self);
+
+    /* ignore-carrier devices ignore all carrier-down events */
+    if (priv->ignore_carrier && !carrier)
+        return;
+
+    if (nm_device_is_master(self)) {
+        if (carrier) {
+            /* Force master to retry getting ip addresses when carrier
+             * is restored. */
+            if (priv->state == NM_DEVICE_STATE_ACTIVATED)
+                nm_device_update_dynamic_ip_setup(self);
+            /* If needed, also resume IP configuration that is
+             * waiting for carrier. */
+            if (nm_device_activate_ip4_state_in_wait(self))
+                nm_device_activate_stage3_ip_start(self, AF_INET);
+            if (nm_device_activate_ip6_state_in_wait(self))
+                nm_device_activate_stage3_ip_start(self, AF_INET6);
+            return;
+        }
+        /* fall-through and change state of device */
+    } else if (priv->is_enslaved && !carrier) {
+        /* Slaves don't deactivate when they lose carrier; for
+         * bonds/teams in particular that would be actively
+         * counterproductive.
+         */
+        return;
+    }
+
+    if (carrier) {
+        if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) {
+            nm_device_queue_state(self,
+                                  NM_DEVICE_STATE_DISCONNECTED,
+                                  NM_DEVICE_STATE_REASON_CARRIER);
+        } else if (priv->state == NM_DEVICE_STATE_DISCONNECTED) {
+            /* If the device is already in DISCONNECTED state without a carrier
+             * (probably because it is tagged for carrier ignore) ensure that
+             * when the carrier appears, auto connections are rechecked for
+             * the device.
+             */
+            nm_device_emit_recheck_auto_activate(self);
+        } else if (priv->state == NM_DEVICE_STATE_ACTIVATED) {
+            /* If the device is active without a carrier (probably because it is
+             * tagged for carrier ignore) ensure that when the carrier appears we
+             * renew DHCP leases and such.
+             */
+            nm_device_update_dynamic_ip_setup(self);
+        }
+    } else {
+        if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) {
+            if (priv->queued_state.id && priv->queued_state.state >= NM_DEVICE_STATE_DISCONNECTED)
+                queued_state_clear(self);
+        } else {
+            nm_device_queue_state(self,
+                                  NM_DEVICE_STATE_UNAVAILABLE,
+                                  NM_DEVICE_STATE_REASON_CARRIER);
+        }
+    }
+}
+
+static gboolean
+carrier_disconnected_action_cb(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    _LOGD(LOGD_DEVICE,
+          "carrier: link disconnected (calling deferred action) (id=%u)",
+          priv->carrier_defer_id);
+
+    priv->carrier_defer_id = 0;
+    carrier_changed(self, FALSE);
+    return FALSE;
+}
+
+static void
+carrier_disconnected_action_cancel(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    guint            id   = priv->carrier_defer_id;
+
+    if (nm_clear_g_source(&priv->carrier_defer_id)) {
+        _LOGD(LOGD_DEVICE, "carrier: link disconnected (canceling deferred action) (id=%u)", id);
+    }
+}
+
+void
+nm_device_set_carrier(NMDevice *self, gboolean carrier)
+{
+    NMDevicePrivate *priv         = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceState    state        = nm_device_get_state(self);
+    gboolean         notify_flags = FALSE;
+
+    if (priv->carrier == carrier)
+        return;
+
+    if (NM_FLAGS_ALL(priv->capabilities,
+                     NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER)) {
+        if (carrier)
+            priv->interface_flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER;
+        else
+            priv->interface_flags &= ~NM_DEVICE_INTERFACE_FLAG_CARRIER;
+        notify_flags = TRUE;
+    }
+
+    priv->carrier = carrier;
+    if (notify_flags)
+        nm_gobject_notify_together(self, PROP_CARRIER, PROP_INTERFACE_FLAGS);
+    else
+        _notify(self, PROP_CARRIER);
+
+    if (priv->carrier) {
+        _LOGI(LOGD_DEVICE, "carrier: link connected");
+        carrier_disconnected_action_cancel(self);
+        NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier);
+        carrier_changed(self, TRUE);
+
+        if (priv->carrier_wait_id) {
+            nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+            _carrier_wait_check_queued_act_request(self);
+        }
+    } else {
+        if (priv->carrier_wait_id)
+            nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+        NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier);
+        if (state <= NM_DEVICE_STATE_DISCONNECTED && !priv->queued_act_request) {
+            _LOGD(LOGD_DEVICE, "carrier: link disconnected");
+            carrier_changed(self, FALSE);
+        } else {
+            gint64 now_ms, until_ms;
+
+            now_ms   = nm_utils_get_monotonic_timestamp_msec();
+            until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
+            priv->carrier_defer_id =
+                g_timeout_add(until_ms - now_ms, carrier_disconnected_action_cb, self);
+            _LOGD(LOGD_DEVICE,
+                  "carrier: link disconnected (deferring action for %ld milliseconds) (id=%u)",
+                  (long) (until_ms - now_ms),
+                  priv->carrier_defer_id);
+        }
+    }
+}
+
+static void
+nm_device_set_carrier_from_platform(NMDevice *self)
+{
+    int ifindex;
+
+    if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)) {
+        if (!nm_device_has_capability(self, NM_DEVICE_CAP_NONSTANDARD_CARRIER)
+            && (ifindex = nm_device_get_ip_ifindex(self)) > 0) {
+            nm_device_set_carrier(
+                self,
+                nm_platform_link_is_connected(nm_device_get_platform(self), ifindex));
+        }
+    } else {
+        NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+        /* Fake online link when carrier detection is not available. */
+        if (!priv->carrier) {
+            priv->carrier = TRUE;
+            _notify(self, PROP_CARRIER);
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
+{
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDevice *           master;
+    nm_auto_nmpobj const NMPObject *plink_master_keep_alive = NULL;
+    const NMPlatformLink *          plink_master;
+
+    g_return_if_fail(plink);
+
+    if (plink->master <= 0)
+        goto out;
+
+    master                  = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, plink->master);
+    plink_master            = nm_platform_link_get(nm_device_get_platform(self), plink->master);
+    plink_master_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink_master));
+
+    if (master == NULL && plink_master && nm_streq0(plink_master->name, "ovs-system")
+        && plink_master->type == NM_LINK_TYPE_OPENVSWITCH) {
+        _LOGD(LOGD_DEVICE, "the device claimed by openvswitch");
+        goto out;
+    }
+
+    priv->master_ifindex = plink->master;
+
+    if (priv->master) {
+        if (plink->master > 0 && plink->master == nm_device_get_ifindex(priv->master)) {
+            /* call add-slave again. We expect @self already to be added to
+             * the master, but this also triggers a recheck-assume. */
+            nm_device_master_add_slave(priv->master, self, FALSE);
+            goto out;
+        }
+
+        nm_device_master_release_one_slave(priv->master,
+                                           self,
+                                           FALSE,
+                                           FALSE,
+                                           NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+    }
+
+    if (master && NM_DEVICE_GET_CLASS(master)->enslave_slave) {
+        nm_device_master_add_slave(master, self, FALSE);
+        goto out;
+    }
+
+    if (master) {
+        _LOGD(LOGD_DEVICE,
+              "enslaved to non-master-type device %s; ignoring",
+              nm_device_get_iface(master));
+    } else {
+        _LOGD(LOGD_DEVICE,
+              "enslaved to unknown device %d (%s%s%s)",
+              plink->master,
+              NM_PRINT_FMT_QUOTED(plink_master, "\"", plink_master->name, "\"", "??"));
+    }
+    if (!priv->ifindex_changed_id) {
+        priv->ifindex_changed_id = g_signal_connect(nm_device_get_manager(self),
+                                                    NM_MANAGER_DEVICE_IFINDEX_CHANGED,
+                                                    G_CALLBACK(device_ifindex_changed_cb),
+                                                    self);
+    }
+    return;
+
+out:
+    nm_clear_g_signal_handler(nm_device_get_manager(self), &priv->ifindex_changed_id);
+}
+
+static void
+device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->master_ifindex != nm_device_get_ifindex(device_changed))
+        return;
+
+    _LOGD(LOGD_DEVICE,
+          "master %s with ifindex %d appeared",
+          nm_device_get_iface(device_changed),
+          nm_device_get_ifindex(device_changed));
+    if (!priv->device_link_changed_id)
+        priv->device_link_changed_id = g_idle_add((GSourceFunc) device_link_changed, self);
+}
+
+static void
+ndisc_set_router_config(NMNDisc *ndisc, NMDevice *self)
+{
+    NMDevicePrivate *priv                    = NM_DEVICE_GET_PRIVATE(self);
+    gs_unref_array GArray *addresses         = NULL;
+    gs_unref_array GArray *dns_servers       = NULL;
+    gs_unref_array GArray *      dns_domains = NULL;
+    guint                        len;
+    guint                        i;
+    const NMDedupMultiHeadEntry *head_entry;
+    NMDedupMultiIter             ipconf_iter;
+
+    if (nm_ndisc_get_node_type(ndisc) != NM_NDISC_NODE_TYPE_ROUTER)
+        return;
+
+    head_entry = nm_ip6_config_lookup_addresses(priv->ip_config_6);
+    addresses =
+        g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscAddress), head_entry ? head_entry->len : 0);
+    nm_dedup_multi_iter_for_each (&ipconf_iter, head_entry) {
+        const NMPlatformIP6Address *addr = NMP_OBJECT_CAST_IP6_ADDRESS(ipconf_iter.current->obj);
+        NMNDiscAddress *            ndisc_addr;
+        guint32                     lifetime;
+        guint32                     preferred;
+
+        if (IN6_IS_ADDR_UNSPECIFIED(&addr->address) || IN6_IS_ADDR_LINKLOCAL(&addr->address))
+            continue;
+
+        if (addr->n_ifa_flags & IFA_F_TENTATIVE || addr->n_ifa_flags & IFA_F_DADFAILED)
+            continue;
+
+        if (addr->plen != 64)
+            continue;
+
+        lifetime = nm_utils_lifetime_get(addr->timestamp,
+                                         addr->lifetime,
+                                         addr->preferred,
+                                         NM_NDISC_EXPIRY_BASE_TIMESTAMP / 1000,
+                                         &preferred);
+        if (!lifetime)
+            continue;
+
+        g_array_set_size(addresses, addresses->len + 1);
+        ndisc_addr          = &g_array_index(addresses, NMNDiscAddress, addresses->len - 1);
+        ndisc_addr->address = addr->address;
+        ndisc_addr->expiry_msec =
+            _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, lifetime);
+        ndisc_addr->expiry_preferred_msec =
+            _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, preferred);
+    }
+
+    len         = nm_ip6_config_get_num_nameservers(priv->ip_config_6);
+    dns_servers = g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscDNSServer), len);
+    g_array_set_size(dns_servers, len);
+    for (i = 0; i < len; i++) {
+        const struct in6_addr *nameserver = nm_ip6_config_get_nameserver(priv->ip_config_6, i);
+        NMNDiscDNSServer *     ndisc_nameserver;
+
+        ndisc_nameserver          = &g_array_index(dns_servers, NMNDiscDNSServer, i);
+        ndisc_nameserver->address = *nameserver;
+        ndisc_nameserver->expiry_msec =
+            _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, NM_NDISC_ROUTER_LIFETIME);
+    }
+
+    len         = nm_ip6_config_get_num_searches(priv->ip_config_6);
+    dns_domains = g_array_sized_new(FALSE, TRUE, sizeof(NMNDiscDNSDomain), len);
+    g_array_set_size(dns_domains, len);
+    for (i = 0; i < len; i++) {
+        const char *      search = nm_ip6_config_get_search(priv->ip_config_6, i);
+        NMNDiscDNSDomain *ndisc_search;
+
+        ndisc_search         = &g_array_index(dns_domains, NMNDiscDNSDomain, i);
+        ndisc_search->domain = (char *) search;
+        ndisc_search->expiry_msec =
+            _nm_ndisc_lifetime_to_expiry(NM_NDISC_EXPIRY_BASE_TIMESTAMP, NM_NDISC_ROUTER_LIFETIME);
+    }
+
+    nm_ndisc_set_config(ndisc, addresses, dns_servers, dns_domains);
+}
+
+static void
+device_update_interface_flags(NMDevice *self, const NMPlatformLink *plink)
+{
+    NMDevicePrivate *      priv  = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceInterfaceFlags flags = NM_DEVICE_INTERFACE_FLAG_NONE;
+
+    if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP))
+        flags |= NM_DEVICE_INTERFACE_FLAG_UP;
+    if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_LOWER_UP))
+        flags |= NM_DEVICE_INTERFACE_FLAG_LOWER_UP;
+
+    if (NM_FLAGS_ALL(priv->capabilities,
+                     NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_NONSTANDARD_CARRIER)) {
+        if (priv->carrier)
+            flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER;
+    } else {
+        if (plink && NM_FLAGS_HAS(plink->n_ifi_flags, IFF_LOWER_UP))
+            flags |= NM_DEVICE_INTERFACE_FLAG_CARRIER;
+    }
+
+    if (flags != priv->interface_flags) {
+        priv->interface_flags = flags;
+        _notify(self, PROP_INTERFACE_FLAGS);
+    }
+}
+
+static gboolean
+device_link_changed(NMDevice *self)
+{
+    NMDeviceClass *      klass                        = NM_DEVICE_GET_CLASS(self);
+    NMDevicePrivate *    priv                         = NM_DEVICE_GET_PRIVATE(self);
+    gboolean             ip_ifname_changed            = FALSE;
+    nm_auto_nmpobj const NMPObject *pllink_keep_alive = NULL;
+    const NMPlatformLink *          pllink;
+    const char *                    str;
+    int                             ifindex;
+    gboolean                        was_up;
+    gboolean                        update_unmanaged_specs = FALSE;
+    gboolean                        got_hw_addr            = FALSE, had_hw_addr;
+    gboolean                        seen_down              = priv->device_link_changed_down;
+
+    priv->device_link_changed_id   = 0;
+    priv->device_link_changed_down = FALSE;
+
+    ifindex = nm_device_get_ifindex(self);
+    if (ifindex <= 0)
+        return G_SOURCE_REMOVE;
+    pllink = nm_platform_link_get(nm_device_get_platform(self), ifindex);
+    if (!pllink)
+        return G_SOURCE_REMOVE;
+
+    pllink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(pllink));
+
+    str = nm_platform_link_get_udi(nm_device_get_platform(self), pllink->ifindex);
+    if (!nm_streq0(str, priv->udi)) {
+        g_free(priv->udi);
+        priv->udi = g_strdup(str);
+        _notify(self, PROP_UDI);
+    }
+
+    str = nm_platform_link_get_path(nm_device_get_platform(self), pllink->ifindex);
+    if (!nm_streq0(str, priv->path)) {
+        g_free(priv->path);
+        priv->path = g_strdup(str);
+        _notify(self, PROP_PATH);
+    }
+
+    if (!nm_streq0(pllink->driver, priv->driver)) {
+        g_free(priv->driver);
+        priv->driver = g_strdup(pllink->driver);
+        _notify(self, PROP_DRIVER);
+    }
+
+    _set_mtu(self, pllink->mtu);
+
+    if (ifindex == nm_device_get_ip_ifindex(self))
+        _stats_update_counters_from_pllink(self, pllink);
+
+    had_hw_addr = (priv->hw_addr != NULL);
+    nm_device_update_hw_address(self);
+    got_hw_addr = (!had_hw_addr && priv->hw_addr);
+    nm_device_update_permanent_hw_address(self, FALSE);
+
+    if (pllink->name[0] && !nm_streq(priv->iface, pllink->name)) {
+        _LOGI(LOGD_DEVICE,
+              "interface index %d renamed iface from '%s' to '%s'",
+              priv->ifindex,
+              priv->iface,
+              pllink->name);
+        g_free(priv->iface_);
+        priv->iface_ = g_strdup(pllink->name);
+
+        /* If the device has no explicit ip_iface, then changing iface changes ip_iface too. */
+        ip_ifname_changed = !priv->ip_iface;
+
+        if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT))
+            nm_device_set_unmanaged_by_user_settings(self);
+        else
+            update_unmanaged_specs = TRUE;
+
+        _notify(self, PROP_IFACE);
+        if (ip_ifname_changed)
+            _notify(self, PROP_IP_IFACE);
+
+        /* Re-match available connections against the new interface name */
+        nm_device_recheck_available_connections(self);
+
+        /* Let any connections that use the new interface name have a chance
+         * to auto-activate on the device.
+         */
+        nm_device_emit_recheck_auto_activate(self);
+    }
+
+    if (priv->ndisc && pllink->inet6_token.id) {
+        if (nm_ndisc_set_iid(priv->ndisc, pllink->inet6_token))
+            _LOGD(LOGD_DEVICE, "IPv6 tokenized identifier present on device %s", priv->iface);
+    }
+
+    /* Update carrier from link event if applicable. */
+    if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)
+        && !nm_device_has_capability(self, NM_DEVICE_CAP_NONSTANDARD_CARRIER))
+        nm_device_set_carrier(self, pllink->connected);
+
+    device_update_interface_flags(self, pllink);
+
+    klass->link_changed(self, pllink);
+
+    /* Update DHCP, etc, if needed */
+    if (ip_ifname_changed)
+        nm_device_update_dynamic_ip_setup(self);
+
+    was_up   = priv->up;
+    priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP);
+
+    if (pllink->initialized && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
+        NMDeviceStateReason reason;
+
+        nm_device_set_unmanaged_by_user_udev(self);
+        nm_device_set_unmanaged_by_user_conf(self);
+
+        reason = NM_DEVICE_STATE_REASON_NOW_MANAGED;
+
+        /* If the device is a external-down candidated but no longer has external
+         * down set, we must clear the platform-unmanaged flag with reason
+         * "assumed". */
+        if (nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN)
+            && !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN)) {
+            /* actually, user-udev overwrites external-down. So we only assume the device,
+             * when it is a external-down candidate, which is not managed via udev. */
+            if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_UDEV)) {
+                /* Ensure the assume check is queued before any queued state changes
+                 * from the transition to UNAVAILABLE.
+                 */
+                nm_device_queue_recheck_assume(self);
+                reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED;
+            }
+        }
+
+        nm_device_set_unmanaged_by_flags(self, NM_UNMANAGED_PLATFORM_INIT, FALSE, reason);
+    }
+
+    set_unmanaged_external_down(self, FALSE);
+
+    device_recheck_slave_status(self, pllink);
+
+    if (priv->up && (!was_up || seen_down)) {
+        /* the link was down and just came up. That happens for example, while changing MTU.
+         * We must restore IP configuration. */
+        if (NM_IN_SET(priv->ip_state_4, NM_DEVICE_IP_STATE_CONF, NM_DEVICE_IP_STATE_DONE)) {
+            if (!ip_config_merge_and_apply(self, AF_INET, TRUE))
+                _LOGW(LOGD_IP4, "failed applying IP4 config after link comes up again");
+        }
+
+        priv->linklocal6_dad_counter = 0;
+        if (NM_IN_SET(priv->ip_state_6, NM_DEVICE_IP_STATE_CONF, NM_DEVICE_IP_STATE_DONE)) {
+            if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+                _LOGW(LOGD_IP6, "failed applying IP6 config after link comes up again");
+        }
+    }
+
+    if (update_unmanaged_specs)
+        nm_device_set_unmanaged_by_user_settings(self);
+
+    if (got_hw_addr && !priv->up && nm_device_get_state(self) == NM_DEVICE_STATE_UNAVAILABLE) {
+        /*
+         * If the device is UNAVAILABLE, any previous try to
+         * bring it up probably has failed because of the
+         * invalid hardware address; try again.
+         */
+        nm_device_bring_up(self, TRUE, NULL);
+        nm_device_queue_recheck_available(self,
+                                          NM_DEVICE_STATE_REASON_NONE,
+                                          NM_DEVICE_STATE_REASON_NONE);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+device_ip_link_changed(NMDevice *self)
+{
+    NMDevicePrivate *     priv = NM_DEVICE_GET_PRIVATE(self);
+    const NMPlatformLink *pllink;
+    const char *          ip_iface;
+
+    priv->device_ip_link_changed_id = 0;
+
+    if (priv->ip_ifindex <= 0)
+        return G_SOURCE_REMOVE;
+
+    nm_assert(priv->ip_iface);
+
+    pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ip_ifindex);
+    if (!pllink)
+        return G_SOURCE_REMOVE;
+
+    if (priv->ifindex <= 0 && pllink->mtu)
+        _set_mtu(self, pllink->mtu);
+
+    _stats_update_counters_from_pllink(self, pllink);
+
+    ip_iface = pllink->name;
+
+    if (!ip_iface[0])
+        return FALSE;
+
+    if (!nm_streq(priv->ip_iface, ip_iface)) {
+        _LOGI(LOGD_DEVICE,
+              "ip-ifname: interface index %d renamed ip_iface (%d) from '%s' to '%s'",
+              priv->ifindex,
+              priv->ip_ifindex,
+              priv->ip_iface,
+              ip_iface);
+        g_free(priv->ip_iface_);
+        priv->ip_iface_ = g_strdup(ip_iface);
+        _notify(self, PROP_IP_IFACE);
+
+        nm_device_update_dynamic_ip_setup(self);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+link_changed_cb(NMPlatform *    platform,
+                int             obj_type_i,
+                int             ifindex,
+                NMPlatformLink *info,
+                int             change_type_i,
+                NMDevice *      self)
+{
+    const NMPlatformSignalChangeType change_type = change_type_i;
+    NMDevicePrivate *                priv;
+
+    if (change_type != NM_PLATFORM_SIGNAL_CHANGED)
+        return;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (ifindex == nm_device_get_ifindex(self)) {
+        if (!(info->n_ifi_flags & IFF_UP))
+            priv->device_link_changed_down = TRUE;
+        if (!priv->device_link_changed_id) {
+            priv->device_link_changed_id = g_idle_add((GSourceFunc) device_link_changed, self);
+            _LOGD(LOGD_DEVICE, "queued link change for ifindex %d", ifindex);
+        }
+    } else if (ifindex == nm_device_get_ip_ifindex(self)) {
+        if (!priv->device_ip_link_changed_id) {
+            priv->device_ip_link_changed_id =
+                g_idle_add((GSourceFunc) device_ip_link_changed, self);
+            _LOGD(LOGD_DEVICE, "queued link change for ip-ifindex %d", ifindex);
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+link_changed(NMDevice *self, const NMPlatformLink *pllink)
+{
+    /* stub implementation of virtual function to allow subclasses to chain up. */
+}
+
+static gboolean
+link_type_compatible(NMDevice *self, NMLinkType link_type, gboolean *out_compatible, GError **error)
+{
+    NMDeviceClass *klass;
+    NMLinkType     device_type;
+    guint          i = 0;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    klass = NM_DEVICE_GET_CLASS(self);
+
+    if (!klass->link_types) {
+        NM_SET_OUT(out_compatible, FALSE);
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_FAILED,
+                            "Device does not support platform links");
+        return FALSE;
+    }
+
+    device_type = self->_priv->link_type;
+    if (device_type > NM_LINK_TYPE_UNKNOWN && device_type != link_type) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "Needed link type 0x%x does not match the platform link type 0x%X",
+                    device_type,
+                    link_type);
+        return FALSE;
+    }
+
+    for (i = 0; klass->link_types[i] > NM_LINK_TYPE_UNKNOWN; i++) {
+        if (klass->link_types[i] == link_type)
+            return TRUE;
+        if (klass->link_types[i] == NM_LINK_TYPE_ANY)
+            return TRUE;
+    }
+
+    NM_SET_OUT(out_compatible, FALSE);
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_FAILED,
+                "Device does not support platform link type 0x%X",
+                link_type);
+    return FALSE;
+}
+
+/**
+ * nm_device_realize_start():
+ * @self: the #NMDevice
+ * @plink: an existing platform link or %NULL
+ * @assume_state_guess_assume: set the guess_assume state.
+ * @assume_state_connection_uuid: set the connection uuid to assume.
+ * @set_nm_owned: for software device, if TRUE set nm-owned.
+ * @unmanaged_user_explicit: the user-explicit unmanaged flag to apply
+ *   on the device initially.
+ * @out_compatible: %TRUE on return if @self is compatible with @plink
+ * @error: location to store error, or %NULL
+ *
+ * Initializes and sets up the device using existing backing resources. Before
+ * the device is ready for use nm_device_realize_finish() must be called.
+ * @out_compatible will only be set if @plink is not %NULL, and
+ *
+ * Important: if nm_device_realize_start() returns %TRUE, the caller MUST
+ * also call nm_device_realize_finish() to balance g_object_freeze_notify().
+ *
+ * Returns: %TRUE on success, %FALSE on error
+ */
+gboolean
+nm_device_realize_start(NMDevice *            self,
+                        const NMPlatformLink *plink,
+                        gboolean              assume_state_guess_assume,
+                        const char *          assume_state_connection_uuid,
+                        gboolean              set_nm_owned,
+                        NMUnmanFlagOp         unmanaged_user_explicit,
+                        gboolean *            out_compatible,
+                        GError **             error)
+{
+    nm_auto_nmpobj const NMPObject *plink_keep_alive = NULL;
+
+    nm_assert(!plink || NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK);
+
+    NM_SET_OUT(out_compatible, TRUE);
+
+    if (plink) {
+        if (!nm_streq0(nm_device_get_iface(self), plink->name)) {
+            NM_SET_OUT(out_compatible, FALSE);
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_FAILED,
+                                "Device interface name does not match platform link");
+            return FALSE;
+        }
+
+        if (!link_type_compatible(self, plink->type, out_compatible, error))
+            return FALSE;
+
+        plink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink));
+    }
+
+    realize_start_setup(self,
+                        plink,
+                        assume_state_guess_assume,
+                        assume_state_connection_uuid,
+                        set_nm_owned,
+                        unmanaged_user_explicit,
+                        FALSE);
+    return TRUE;
+}
+
+/**
+ * nm_device_create_and_realize():
+ * @self: the #NMDevice
+ * @connection: the #NMConnection being activated
+ * @parent: the parent #NMDevice if any
+ * @error: location to store error, or %NULL
+ *
+ * Creates any backing resources needed to realize the device to proceed
+ * with activating @connection.
+ *
+ * Returns: %TRUE on success, %FALSE on error
+ */
+gboolean
+nm_device_create_and_realize(NMDevice *    self,
+                             NMConnection *connection,
+                             NMDevice *    parent,
+                             GError **     error)
+{
+    nm_auto_nmpobj const NMPObject *plink_keep_alive = NULL;
+    NMDevicePrivate *               priv             = NM_DEVICE_GET_PRIVATE(self);
+    const NMPlatformLink *          plink;
+    gboolean                        nm_owned;
+
+    /* Must be set before device is realized */
+    plink    = nm_platform_link_get_by_ifname(nm_device_get_platform(self), priv->iface);
+    nm_owned = !plink || !link_type_compatible(self, plink->type, NULL, NULL);
+    _LOGD(LOGD_DEVICE, "create (is %snm-owned)", nm_owned ? "" : "not ");
+
+    plink = NULL;
+    /* Create any resources the device needs */
+    if (NM_DEVICE_GET_CLASS(self)->create_and_realize) {
+        if (!NM_DEVICE_GET_CLASS(self)->create_and_realize(self, connection, parent, &plink, error))
+            return FALSE;
+        if (plink) {
+            nm_assert(NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK);
+            plink_keep_alive = nmp_object_ref(NMP_OBJECT_UP_CAST(plink));
+        }
+    }
+
+    priv->nm_owned = nm_owned;
+
+    realize_start_setup(self,
+                        plink,
+                        FALSE, /* assume_state_guess_assume */
+                        NULL,  /* assume_state_connection_uuid */
+                        FALSE,
+                        NM_UNMAN_FLAG_OP_FORGET,
+                        TRUE);
+    nm_device_realize_finish(self, plink);
+
+    if (nm_device_get_managed(self, FALSE)) {
+        nm_device_state_changed(self,
+                                NM_DEVICE_STATE_UNAVAILABLE,
+                                NM_DEVICE_STATE_REASON_NOW_MANAGED);
+    }
+    return TRUE;
+}
+
+static gboolean
+can_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink)
+{
+    return TRUE;
+}
+
+void
+nm_device_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    const char *     str;
+    gboolean         ifindex_changed;
+    guint32          mtu;
+
+    if (!NM_DEVICE_GET_CLASS(self)->can_update_from_platform_link(self, plink))
+        return;
+
+    g_return_if_fail(plink == NULL || link_type_compatible(self, plink->type, NULL, NULL));
+
+    str = plink ? nm_platform_link_get_udi(nm_device_get_platform(self), plink->ifindex) : NULL;
+    if (!nm_streq0(str, priv->udi)) {
+        g_free(priv->udi);
+        priv->udi = g_strdup(str);
+        _notify(self, PROP_UDI);
+    }
+
+    str = plink ? nm_platform_link_get_path(nm_device_get_platform(self), plink->ifindex) : NULL;
+    if (!nm_streq0(str, priv->path)) {
+        g_free(priv->path);
+        priv->path = g_strdup(str);
+        _notify(self, PROP_PATH);
+    }
+
+    if (plink && !nm_str_is_empty(plink->name) && nm_utils_strdup_reset(&priv->iface_, plink->name))
+        _notify(self, PROP_IFACE);
+
+    str = plink ? plink->driver : NULL;
+    if (!nm_streq0(str, priv->driver)) {
+        g_free(priv->driver);
+        priv->driver = g_strdup(str);
+        _notify(self, PROP_DRIVER);
+    }
+
+    if (plink) {
+        priv->up = NM_FLAGS_HAS(plink->n_ifi_flags, IFF_UP);
+        if (plink->ifindex == nm_device_get_ip_ifindex(self))
+            _stats_update_counters_from_pllink(self, plink);
+    } else {
+        priv->up = FALSE;
+    }
+
+    mtu = plink ? plink->mtu : 0;
+    _set_mtu(self, mtu);
+
+    ifindex_changed = _set_ifindex(self, plink ? plink->ifindex : 0, FALSE);
+
+    if (ifindex_changed)
+        NM_DEVICE_GET_CLASS(self)->link_changed(self, plink);
+
+    device_update_interface_flags(self, plink);
+}
+
+/*****************************************************************************/
+
+static void
+sriov_op_start(NMDevice *self, SriovOp *op)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(!priv->sriov.pending);
+
+    op->cancellable     = g_cancellable_new();
+    op->device          = g_object_ref(self);
+    priv->sriov.pending = op;
+
+    nm_platform_link_set_sriov_params_async(nm_device_get_platform(self),
+                                            priv->ifindex,
+                                            op->num_vfs,
+                                            op->autoprobe,
+                                            sriov_op_cb,
+                                            op,
+                                            op->cancellable);
+}
+
+static void
+sriov_op_cb(GError *error, gpointer user_data)
+{
+    SriovOp *       op             = user_data;
+    gs_unref_object NMDevice *self = op->device;
+    NMDevicePrivate *         priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(op == priv->sriov.pending);
+
+    g_clear_object(&op->cancellable);
+
+    if (op->callback)
+        op->callback(error, op->callback_data);
+
+    priv->sriov.pending = NULL;
+    nm_g_slice_free(op);
+
+    if (priv->sriov.next) {
+        sriov_op_start(self, g_steal_pointer(&priv->sriov.next));
+    }
+}
+
+static void
+sriov_op_queue_op(NMDevice *self, SriovOp *op)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->sriov.next) {
+        SriovOp *op_next = g_steal_pointer(&priv->sriov.next);
+
+        priv->sriov.next = op;
+
+        /* Cancel the next operation immediately */
+        if (op_next->callback) {
+            gs_free_error GError *error = NULL;
+
+            nm_utils_error_set_cancelled(&error, FALSE, NULL);
+            op_next->callback(error, op_next->callback_data);
+        }
+
+        nm_g_slice_free(op_next);
+        return;
+    }
+
+    if (priv->sriov.pending) {
+        priv->sriov.next = op;
+        g_cancellable_cancel(priv->sriov.pending->cancellable);
+        return;
+    }
+
+    if (op)
+        sriov_op_start(self, op);
+}
+
+static void
+sriov_op_queue(NMDevice *              self,
+               guint                   num_vfs,
+               NMOptionBool            autoprobe,
+               NMPlatformAsyncCallback callback,
+               gpointer                callback_data)
+{
+    SriovOp *op;
+
+    /* We usually never want to cancel an async write operation, unless it's superseded
+     * by a newer operation (that resets the state). That is, because we need to ensure
+     * that we never end up doing two concurrent writes (since we write on a background
+     * thread, that would be unordered/racy).
+     * Of course, since we queue requests only per-device, when devices get renamed we
+     * might end up writing the same sysctl concurrently still. But that's really
+     * unlikely, and don't rename after udev completes!
+     *
+     * The "next" operation is not yet even started. It can be replaced/canceled right away
+     * when a newer request comes.
+     * The "pending" operation is currently ongoing, and we may cancel it if
+     * we have a follow-up operation (queued in "next"). Unless we have a such
+     * a newer request, we cannot cancel it!
+     *
+     * FIXME(shutdown): However, during shutdown we don't have a follow-up write request to cancel
+     * this operation and we have to give it at least some time to complete. The solution is that
+     * we register a way to abort the last call during shutdown, and after NM_SHUTDOWN_TIMEOUT_MS
+     * grace period we pull the plug and cancel it. */
+
+    op  = g_slice_new(SriovOp);
+    *op = (SriovOp){
+        .num_vfs       = num_vfs,
+        .autoprobe     = autoprobe,
+        .callback      = callback,
+        .callback_data = callback_data,
+    };
+    sriov_op_queue_op(self, op);
+}
+
+static void
+device_init_static_sriov_num_vfs(NMDevice *self)
+{
+    NMDevicePrivate *priv  = NM_DEVICE_GET_PRIVATE(self);
+    gs_free char *   value = NULL;
+    int              num_vfs;
+
+    if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)) {
+        value   = nm_config_data_get_device_config(NM_CONFIG_GET_DATA,
+                                                 NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
+                                                 self,
+                                                 NULL);
+        num_vfs = _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXINT32, -1);
+        if (num_vfs >= 0)
+            sriov_op_queue(self, num_vfs, NM_OPTION_BOOL_DEFAULT, NULL, NULL);
+    }
+}
+
+static void
+config_changed(NMConfig *          config,
+               NMConfigData *      config_data,
+               NMConfigChangeFlags changes,
+               NMConfigData *      old_data,
+               NMDevice *          self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->state <= NM_DEVICE_STATE_DISCONNECTED || priv->state > NM_DEVICE_STATE_ACTIVATED) {
+        priv->ignore_carrier = nm_config_data_get_ignore_carrier(config_data, self);
+        if (NM_FLAGS_HAS(changes, NM_CONFIG_CHANGE_VALUES))
+            device_init_static_sriov_num_vfs(self);
+    }
+}
+
+static void
+realize_start_notify(NMDevice *self, const NMPlatformLink *pllink)
+{
+    /* the default implementation of realize_start_notify() just calls
+     * link_changed() -- which by default does nothing. */
+    NM_DEVICE_GET_CLASS(self)->link_changed(self, pllink);
+}
+
+/**
+ * realize_start_setup():
+ * @self: the #NMDevice
+ * @plink: the #NMPlatformLink if backed by a kernel netdevice
+ * @assume_state_guess_assume: set the guess_assume state.
+ * @assume_state_connection_uuid: set the connection uuid to assume.
+ * @set_nm_owned: if TRUE and device is a software-device, set nm-owned.
+ *    TRUE.
+ * @unmanaged_user_explicit: the user-explict unmanaged flag to set.
+ * @force_platform_init: if TRUE the platform-init unmanaged flag is
+ *    forcefully cleared.
+ *
+ * Update the device from backing resource properties (like hardware
+ * addresses, carrier states, driver/firmware info, etc).  This function
+ * should only change properties for this device, and should not perform
+ * any tasks that affect other interfaces (like master/slave or parent/child
+ * stuff).
+ */
+static void
+realize_start_setup(NMDevice *            self,
+                    const NMPlatformLink *plink,
+                    gboolean              assume_state_guess_assume,
+                    const char *          assume_state_connection_uuid,
+                    gboolean              set_nm_owned,
+                    NMUnmanFlagOp         unmanaged_user_explicit,
+                    gboolean              force_platform_init)
+{
+    NMDevicePrivate *    priv;
+    NMDeviceClass *      klass;
+    NMPlatform *         platform;
+    NMDeviceCapabilities capabilities = 0;
+    NMConfig *           config;
+    guint                real_rate;
+    gboolean             unmanaged;
+
+    /* plink is a NMPlatformLink type, however, we require it to come from the platform
+     * cache (where else would it come from?). */
+    nm_assert(!plink || NMP_OBJECT_GET_TYPE(NMP_OBJECT_UP_CAST(plink)) == NMP_OBJECT_TYPE_LINK);
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* The device should not be realized */
+    g_return_if_fail(!priv->real);
+    g_return_if_fail(nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT));
+    g_return_if_fail(priv->ip_ifindex <= 0);
+    g_return_if_fail(priv->ip_iface == NULL);
+    g_return_if_fail(!priv->queued_ip_config_id_4);
+    g_return_if_fail(!priv->queued_ip_config_id_6);
+
+    _LOGD(LOGD_DEVICE,
+          "start setup of %s, kernel ifindex %d",
+          G_OBJECT_TYPE_NAME(self),
+          plink ? plink->ifindex : 0);
+
+    klass    = NM_DEVICE_GET_CLASS(self);
+    platform = nm_device_get_platform(self);
+
+    /* Balanced by a thaw in nm_device_realize_finish() */
+    g_object_freeze_notify(G_OBJECT(self));
+
+    priv->mtu_source      = NM_DEVICE_MTU_SOURCE_NONE;
+    priv->mtu_initial     = 0;
+    priv->ip6_mtu_initial = 0;
+    priv->ip6_mtu         = 0;
+    _set_mtu(self, 0);
+
+    _assume_state_set(self, assume_state_guess_assume, assume_state_connection_uuid);
+
+    nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_EXTERNAL);
+
+    if (plink)
+        nm_device_update_from_platform_link(self, plink);
+
+    if (priv->ifindex > 0) {
+        priv->physical_port_id = nm_platform_link_get_physical_port_id(platform, priv->ifindex);
+        _notify(self, PROP_PHYSICAL_PORT_ID);
+
+        priv->dev_id = nm_platform_link_get_dev_id(platform, priv->ifindex);
+
+        if (nm_platform_link_is_software(platform, priv->ifindex))
+            capabilities |= NM_DEVICE_CAP_IS_SOFTWARE;
+
+        _set_mtu(self, nm_platform_link_get_mtu(platform, priv->ifindex));
+
+        nm_platform_link_get_driver_info(platform,
+                                         priv->ifindex,
+                                         NULL,
+                                         &priv->driver_version,
+                                         &priv->firmware_version);
+        if (priv->driver_version)
+            _notify(self, PROP_DRIVER_VERSION);
+        if (priv->firmware_version)
+            _notify(self, PROP_FIRMWARE_VERSION);
+
+        if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL))
+            priv->ipv6ll_handle = nm_platform_link_get_user_ipv6ll_enabled(platform, priv->ifindex);
+
+        if (nm_platform_link_supports_sriov(platform, priv->ifindex))
+            capabilities |= NM_DEVICE_CAP_SRIOV;
+    }
+
+    if (klass->get_generic_capabilities)
+        capabilities |= klass->get_generic_capabilities(self);
+
+    _add_capabilities(self, capabilities);
+
+    if (!priv->nm_owned && set_nm_owned && nm_device_is_software(self)) {
+        priv->nm_owned = TRUE;
+        _LOGD(LOGD_DEVICE, "set nm-owned from state file");
+    }
+
+    if (!priv->udi) {
+        /* Use a placeholder UDI until we get a real one */
+        if (priv->udi_id == 0) {
+            static guint64 udi_id_counter = 0;
+
+            priv->udi_id = ++udi_id_counter;
+        }
+        priv->udi = g_strdup_printf("/virtual/device/placeholder/%" G_GUINT64_FORMAT, priv->udi_id);
+        _notify(self, PROP_UDI);
+    }
+
+    nm_device_update_hw_address(self);
+    nm_device_update_initial_hw_address(self);
+    nm_device_update_permanent_hw_address(self, FALSE);
+
+    /* Note: initial hardware address must be read before calling get_ignore_carrier() */
+    config               = nm_config_get();
+    priv->ignore_carrier = nm_config_data_get_ignore_carrier(nm_config_get_data(config), self);
+    if (!priv->config_changed_id) {
+        priv->config_changed_id = g_signal_connect(config,
+                                                   NM_CONFIG_SIGNAL_CONFIG_CHANGED,
+                                                   G_CALLBACK(config_changed),
+                                                   self);
+    }
+
+    nm_device_set_carrier_from_platform(self);
+
+    nm_assert(!priv->stats.timeout_id);
+    real_rate = _stats_refresh_rate_real(priv->stats.refresh_rate_ms);
+    if (real_rate)
+        priv->stats.timeout_id = g_timeout_add(real_rate, _stats_timeout_cb, self);
+
+    klass->realize_start_notify(self, plink);
+
+    nm_assert(!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_EXPLICIT));
+    nm_device_set_unmanaged_flags(self, NM_UNMANAGED_USER_EXPLICIT, unmanaged_user_explicit);
+
+    /* Do not manage externally created software devices until they are IFF_UP
+     * or have IP addressing */
+    nm_device_set_unmanaged_flags(self,
+                                  NM_UNMANAGED_EXTERNAL_DOWN,
+                                  is_unmanaged_external_down(self, TRUE));
+
+    /* Unmanaged the loopback device with an explicit NM_UNMANAGED_BY_TYPE flag.
+     * Later we might want to manage 'lo' too. Currently, that doesn't work because
+     * NetworkManager might down the interface or remove the 127.0.0.1 address. */
+    nm_device_set_unmanaged_flags(self, NM_UNMANAGED_BY_TYPE, is_loopback(self));
+
+    nm_device_set_unmanaged_by_user_udev(self);
+    nm_device_set_unmanaged_by_user_conf(self);
+
+    unmanaged = plink && !plink->initialized && !force_platform_init;
+
+    nm_device_set_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT, unmanaged);
+}
+
+/**
+ * nm_device_realize_finish():
+ * @self: the #NMDevice
+ * @plink: the #NMPlatformLink if backed by a kernel netdevice
+ *
+ * Update the device's master/slave or parent/child relationships from
+ * backing resource properties.  After this function finishes, the device
+ * is ready for network connectivity.
+ */
+void
+nm_device_realize_finish(NMDevice *self, const NMPlatformLink *plink)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(!plink || link_type_compatible(self, plink->type, NULL, NULL));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_if_fail(!priv->real);
+
+    if (plink)
+        device_recheck_slave_status(self, plink);
+
+    priv->update_ip_config_completed_v4 = FALSE;
+    priv->update_ip_config_completed_v6 = FALSE;
+
+    priv->real = TRUE;
+    _notify(self, PROP_REAL);
+
+    nm_device_recheck_available_connections(self);
+
+    /* Balanced by a freeze in realize_start_setup(). */
+    g_object_thaw_notify(G_OBJECT(self));
+}
+
+static void
+unrealize_notify(NMDevice *self)
+{
+    /* Stub implementation for unrealize_notify(). It does nothing,
+     * but allows derived classes to uniformly invoke the parent
+     * implementation. */
+}
+
+static gboolean
+available_connections_check_delete_unrealized_on_idle(gpointer user_data)
+{
+    NMDevice *       self = user_data;
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->check_delete_unrealized_id = 0;
+
+    if (g_hash_table_size(priv->available_connections) == 0 && !nm_device_is_real(self))
+        g_signal_emit(self, signals[REMOVED], 0);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+available_connections_check_delete_unrealized(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* always rescheadule the remove signal. */
+    nm_clear_g_source(&priv->check_delete_unrealized_id);
+
+    if (g_hash_table_size(priv->available_connections) == 0 && !nm_device_is_real(self))
+        priv->check_delete_unrealized_id =
+            g_idle_add(available_connections_check_delete_unrealized_on_idle, self);
+}
+
+/**
+ * nm_device_unrealize():
+ * @self: the #NMDevice
+ * @remove_resources: if %TRUE, remove backing resources
+ * @error: location to store error, or %NULL
+ *
+ * Clears any properties that depend on backing resources (kernel devices,
+ * etc) and removes those resources if @remove_resources is %TRUE.
+ *
+ * Returns: %TRUE on success, %FALSE on error
+ */
+gboolean
+nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error)
+{
+    NMDevicePrivate *priv;
+    int              ifindex;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    if (!nm_device_is_software(self) || !nm_device_is_real(self)) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_NOT_SOFTWARE,
+                            "This device is not a software device or is not realized");
+        return FALSE;
+    }
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->iface != NULL, FALSE);
+    g_return_val_if_fail(priv->real, FALSE);
+
+    ifindex = nm_device_get_ifindex(self);
+
+    _LOGD(LOGD_DEVICE, "unrealize (ifindex %d)", ifindex > 0 ? ifindex : 0);
+
+    nm_device_assume_state_reset(self);
+
+    if (remove_resources) {
+        if (NM_DEVICE_GET_CLASS(self)->unrealize) {
+            if (!NM_DEVICE_GET_CLASS(self)->unrealize(self, error))
+                return FALSE;
+        } else if (ifindex > 0) {
+            nm_platform_link_delete(nm_device_get_platform(self), ifindex);
+        }
+    }
+
+    nm_clear_g_source(&priv->queued_ip_config_id_4);
+    nm_clear_g_source(&priv->queued_ip_config_id_6);
+
+    g_object_freeze_notify(G_OBJECT(self));
+    NM_DEVICE_GET_CLASS(self)->unrealize_notify(self);
+
+    _parent_set_ifindex(self, 0, FALSE);
+
+    _set_ifindex(self, 0, FALSE);
+    _set_ifindex(self, 0, TRUE);
+    if (nm_clear_g_free(&priv->ip_iface_))
+        _notify(self, PROP_IP_IFACE);
+
+    priv->master_ifindex = 0;
+
+    _set_mtu(self, 0);
+
+    if (priv->driver_version) {
+        nm_clear_g_free(&priv->driver_version);
+        _notify(self, PROP_DRIVER_VERSION);
+    }
+    if (priv->firmware_version) {
+        nm_clear_g_free(&priv->firmware_version);
+        _notify(self, PROP_FIRMWARE_VERSION);
+    }
+    if (priv->udi) {
+        nm_clear_g_free(&priv->udi);
+        _notify(self, PROP_UDI);
+    }
+    if (priv->path) {
+        nm_clear_g_free(&priv->path);
+        _notify(self, PROP_PATH);
+    }
+    if (priv->physical_port_id) {
+        nm_clear_g_free(&priv->physical_port_id);
+        _notify(self, PROP_PHYSICAL_PORT_ID);
+    }
+
+    nm_clear_g_source(&priv->stats.timeout_id);
+    _stats_update_counters(self, 0, 0);
+
+    priv->hw_addr_len_ = 0;
+    if (nm_clear_g_free(&priv->hw_addr))
+        _notify(self, PROP_HW_ADDRESS);
+    priv->hw_addr_type = HW_ADDR_TYPE_UNSET;
+    if (nm_clear_g_free(&priv->hw_addr_perm))
+        _notify(self, PROP_PERM_HW_ADDRESS);
+    nm_clear_g_free(&priv->hw_addr_initial);
+
+    priv->capabilities = NM_DEVICE_CAP_NM_SUPPORTED;
+    if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities)
+        priv->capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self);
+    _notify(self, PROP_CAPABILITIES);
+
+    nm_clear_g_signal_handler(nm_config_get(), &priv->config_changed_id);
+    nm_clear_g_signal_handler(priv->manager, &priv->ifindex_changed_id);
+
+    priv->real = FALSE;
+    _notify(self, PROP_REAL);
+
+    g_object_thaw_notify(G_OBJECT(self));
+
+    nm_device_set_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT, TRUE);
+
+    nm_device_set_unmanaged_flags(self,
+                                  NM_UNMANAGED_PARENT | NM_UNMANAGED_BY_TYPE
+                                      | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_EXPLICIT
+                                      | NM_UNMANAGED_EXTERNAL_DOWN | NM_UNMANAGED_IS_SLAVE,
+                                  NM_UNMAN_FLAG_OP_FORGET);
+
+    nm_device_state_changed(self,
+                            NM_DEVICE_STATE_UNMANAGED,
+                            remove_resources ? NM_DEVICE_STATE_REASON_USER_REQUESTED
+                                             : NM_DEVICE_STATE_REASON_NOW_UNMANAGED);
+
+    /* Garbage-collect unneeded unrealized devices. */
+    nm_device_recheck_available_connections(self);
+
+    return TRUE;
+}
+
+void
+nm_device_notify_availability_maybe_changed(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->state != NM_DEVICE_STATE_DISCONNECTED)
+        return;
+
+    /* A device could have stayed disconnected because it would
+     * want to register with a network server that now become
+     * available. */
+    nm_device_recheck_available_connections(self);
+    if (g_hash_table_size(priv->available_connections) > 0)
+        nm_device_emit_recheck_auto_activate(self);
+}
+
+/**
+ * nm_device_owns_iface():
+ * @self: the #NMDevice
+ * @iface: an interface name
+ *
+ * Called by the manager to ask if the device or any of its components owns
+ * @iface.  For example, a WWAN implementation would return %TRUE for an
+ * ethernet interface name that was owned by the WWAN device's modem component,
+ * because that ethernet interface is controlled by the WWAN device and cannot
+ * be used independently of the WWAN device.
+ *
+ * Returns: %TRUE if @self or its components own the interface name,
+ * %FALSE if not
+ */
+gboolean
+nm_device_owns_iface(NMDevice *self, const char *iface)
+{
+    if (NM_DEVICE_GET_CLASS(self)->owns_iface)
+        return NM_DEVICE_GET_CLASS(self)->owns_iface(self, iface);
+    return FALSE;
+}
+
+NMConnection *
+nm_device_new_default_connection(NMDevice *self)
+{
+    NMConnection *connection;
+    GError *      error = NULL;
+
+    if (!NM_DEVICE_GET_CLASS(self)->new_default_connection)
+        return NULL;
+
+    connection = NM_DEVICE_GET_CLASS(self)->new_default_connection(self);
+    if (!connection)
+        return NULL;
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
+        _LOGD(LOGD_DEVICE, "device generated an invalid default connection: %s", error->message);
+        g_error_free(error);
+        g_return_val_if_reached(NULL);
+    }
+
+    return connection;
+}
+
+static void
+slave_state_changed(NMDevice *          slave,
+                    NMDeviceState       slave_new_state,
+                    NMDeviceState       slave_old_state,
+                    NMDeviceStateReason reason,
+                    NMDevice *          self)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         release = FALSE;
+    gboolean         configure;
+
+    _LOGD(LOGD_DEVICE,
+          "slave %s state change %d (%s) -> %d (%s)",
+          nm_device_get_iface(slave),
+          slave_old_state,
+          nm_device_state_to_str(slave_old_state),
+          slave_new_state,
+          nm_device_state_to_str(slave_new_state));
+
+    /* Don't try to enslave slaves until the master is ready */
+    if (priv->state < NM_DEVICE_STATE_CONFIG)
+        return;
+
+    if (slave_new_state == NM_DEVICE_STATE_IP_CONFIG)
+        nm_device_master_enslave_slave(self, slave, nm_device_get_applied_connection(slave));
+    else if (slave_new_state > NM_DEVICE_STATE_ACTIVATED)
+        release = TRUE;
+    else if (slave_new_state <= NM_DEVICE_STATE_DISCONNECTED
+             && slave_old_state > NM_DEVICE_STATE_DISCONNECTED) {
+        /* Catch failures due to unavailable or unmanaged */
+        release = TRUE;
+    }
+
+    if (release) {
+        configure = priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED
+                    && nm_device_sys_iface_state_get(slave) != NM_DEVICE_SYS_IFACE_STATE_EXTERNAL;
+
+        nm_device_master_release_one_slave(self, slave, configure, FALSE, reason);
+        /* Bridge/bond/team interfaces are left up until manually deactivated */
+        if (c_list_is_empty(&priv->slaves) && priv->state == NM_DEVICE_STATE_ACTIVATED)
+            _LOGD(LOGD_DEVICE, "last slave removed; remaining activated");
+    }
+}
+
+/**
+ * nm_device_master_add_slave:
+ * @self: the master device
+ * @slave: the slave device to enslave
+ * @configure: pass %TRUE if the slave should be configured by the master, or
+ * %FALSE if it is already configured outside NetworkManager
+ *
+ * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
+ * etc) then this function adds @slave to the slave list for later enslavement.
+ *
+ * Returns: %TRUE if the slave was enslaved. %FALSE means, the slave was already
+ *   enslaved and nothing was done.
+ */
+static gboolean
+nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
+{
+    NMDevicePrivate *priv;
+    NMDevicePrivate *slave_priv;
+    SlaveInfo *      info;
+    gboolean         changed = FALSE;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_DEVICE(slave), FALSE);
+    g_return_val_if_fail(NM_DEVICE_GET_CLASS(self)->enslave_slave != NULL, FALSE);
+
+    priv       = NM_DEVICE_GET_PRIVATE(self);
+    slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+
+    info = find_slave_info(self, slave);
+
+    _LOGT(LOGD_CORE,
+          "master: add one slave %p/%s%s",
+          slave,
+          nm_device_get_iface(slave),
+          info ? " (already registered)" : "");
+
+    if (configure)
+        g_return_val_if_fail(nm_device_get_state(slave) >= NM_DEVICE_STATE_DISCONNECTED, FALSE);
+
+    if (!info) {
+        g_return_val_if_fail(!slave_priv->master, FALSE);
+        g_return_val_if_fail(!slave_priv->is_enslaved, FALSE);
+
+        info            = g_slice_new0(SlaveInfo);
+        info->slave     = g_object_ref(slave);
+        info->configure = configure;
+        info->watch_id =
+            g_signal_connect(slave, NM_DEVICE_STATE_CHANGED, G_CALLBACK(slave_state_changed), self);
+        c_list_link_tail(&priv->slaves, &info->lst_slave);
+        slave_priv->master = g_object_ref(self);
+
+        _active_connection_set_state_flags(self, NM_ACTIVATION_STATE_FLAG_MASTER_HAS_SLAVES);
+
+        /* no need to emit
+         *
+         *   _notify (slave, PROP_MASTER);
+         *
+         * because slave_priv->is_enslaved is not true, thus the value
+         * didn't change yet. */
+
+        g_warn_if_fail(!NM_FLAGS_HAS(slave_priv->unmanaged_mask, NM_UNMANAGED_IS_SLAVE));
+        nm_device_set_unmanaged_by_flags(slave,
+                                         NM_UNMANAGED_IS_SLAVE,
+                                         FALSE,
+                                         NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+        changed = TRUE;
+    } else
+        g_return_val_if_fail(slave_priv->master == self, FALSE);
+
+    nm_device_queue_recheck_assume(self);
+    nm_device_queue_recheck_assume(slave);
+
+    return changed;
+}
+
+/**
+ * nm_device_master_check_slave_physical_port:
+ * @self: the master device
+ * @slave: a slave device
+ * @log_domain: domain to log a warning in
+ *
+ * Checks if @self already has a slave with the same #NMDevice:physical-port-id
+ * as @slave, and logs a warning if so.
+ */
+void
+nm_device_master_check_slave_physical_port(NMDevice *self, NMDevice *slave, NMLogDomain log_domain)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    const char *     slave_physical_port_id, *existing_physical_port_id;
+    SlaveInfo *      info;
+    CList *          iter;
+
+    slave_physical_port_id = nm_device_get_physical_port_id(slave);
+    if (!slave_physical_port_id)
+        return;
+
+    c_list_for_each (iter, &priv->slaves) {
+        info = c_list_entry(iter, SlaveInfo, lst_slave);
+        if (info->slave == slave)
+            continue;
+
+        existing_physical_port_id = nm_device_get_physical_port_id(info->slave);
+        if (nm_streq0(slave_physical_port_id, existing_physical_port_id)) {
+            _LOGW(log_domain,
+                  "slave %s shares a physical port with existing slave %s",
+                  nm_device_get_ip_iface(slave),
+                  nm_device_get_ip_iface(info->slave));
+            /* Since this function will get called for every slave, we only have
+             * to warn about the first match we find; if there are other matches
+             * later in the list, we will have already warned about them matching
+             * @existing earlier.
+             */
+            return;
+        }
+    }
+}
+
+/* release all slaves */
+void
+nm_device_master_release_slaves(NMDevice *self)
+{
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceStateReason reason;
+    CList *             iter, *safe;
+
+    /* Don't release the slaves if this connection doesn't belong to NM. */
+    if (nm_device_sys_iface_state_is_external(self))
+        return;
+
+    reason = priv->state_reason;
+    if (priv->state == NM_DEVICE_STATE_FAILED)
+        reason = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED;
+
+    c_list_for_each_safe (iter, safe, &priv->slaves) {
+        SlaveInfo *info = c_list_entry(iter, SlaveInfo, lst_slave);
+
+        nm_device_master_release_one_slave(self, info->slave, TRUE, FALSE, reason);
+    }
+}
+
+/**
+ * nm_device_is_master:
+ * @self: the device
+ *
+ * Returns: %TRUE if the device can have slaves
+ */
+gboolean
+nm_device_is_master(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    return NM_DEVICE_GET_CLASS(self)->is_master;
+}
+
+/**
+ * nm_device_get_master:
+ * @self: the device
+ *
+ * If @self has been enslaved by another device, this returns that
+ * device. Otherwise, it returns %NULL. (In particular, note that if
+ * @self is in the process of activating as a slave, but has not yet
+ * been enslaved by its master, this will return %NULL.)
+ *
+ * Returns: (transfer none): @self's master, or %NULL
+ */
+NMDevice *
+nm_device_get_master(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->is_enslaved) {
+        g_return_val_if_fail(priv->master, NULL);
+        return priv->master;
+    }
+    return NULL;
+}
+
+static gboolean
+get_ip_config_may_fail(NMDevice *self, int addr_family)
+{
+    NMConnection *     connection;
+    NMSettingIPConfig *s_ip;
+
+    connection = nm_device_get_applied_connection(self);
+
+    s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+
+    return !s_ip || nm_setting_ip_config_get_may_fail(s_ip);
+}
+
+/*
+ * check_ip_state
+ *
+ * When @full_state_update is TRUE, transition the device from IP_CONFIG to the
+ * next state according to the outcome of IPv4 and IPv6 configuration. @may_fail
+ * indicates that we are called just after the initial configuration and thus
+ * IPv4/IPv6 are allowed to fail if the ipvx.may-fail properties say so, because
+ * the IP methods couldn't even be started.
+ * If @full_state_update is FALSE, just check if the connection should be failed
+ * due to the state of both ip families and the ipvx.may-fail settings.
+ */
+static void
+check_ip_state(NMDevice *self, gboolean may_fail, gboolean full_state_update)
+{
+    NMDevicePrivate *  priv         = NM_DEVICE_GET_PRIVATE(self);
+    gboolean           ip4_disabled = FALSE, ip6_disabled = FALSE;
+    NMSettingIPConfig *s_ip4, *s_ip6;
+    NMDeviceState      state;
+
+    if (full_state_update && nm_device_get_state(self) != NM_DEVICE_STATE_IP_CONFIG)
+        return;
+
+    /* Don't progress into IP_CHECK or SECONDARIES if we're waiting for the
+     * master to enslave us. */
+    if (nm_active_connection_get_master(NM_ACTIVE_CONNECTION(priv->act_request.obj))
+        && !priv->is_enslaved)
+        return;
+
+    s_ip4 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP4_CONFIG);
+    if (s_ip4
+        && nm_streq0(nm_setting_ip_config_get_method(s_ip4), NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
+        ip4_disabled = TRUE;
+
+    s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+    if (s_ip6
+        && NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip6),
+                        NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                        NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
+        ip6_disabled = TRUE;
+
+    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE
+        && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) {
+        /* Both method completed (or disabled), proceed with activation */
+        nm_device_state_changed(self, NM_DEVICE_STATE_IP_CHECK, NM_DEVICE_STATE_REASON_NONE);
+        return;
+    }
+
+    if ((priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL
+         || (ip4_disabled && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE))
+        && (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL
+            || (ip6_disabled && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE))) {
+        /* Either both methods failed, or only one failed and the other is
+         * disabled */
+        if (nm_device_sys_iface_state_is_external_or_assume(self)) {
+            /* We have assumed configuration, but couldn't redo it. No problem,
+             * move to check state. */
+            _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_DONE);
+            _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_DONE);
+            state = NM_DEVICE_STATE_IP_CHECK;
+        } else if (may_fail && get_ip_config_may_fail(self, AF_INET)
+                   && get_ip_config_may_fail(self, AF_INET6)) {
+            /* Couldn't start either IPv6 and IPv4 autoconfiguration,
+             * but both are allowed to fail. */
+            state = NM_DEVICE_STATE_SECONDARIES;
+        } else {
+            /* Autoconfiguration attempted without success. */
+            state = NM_DEVICE_STATE_FAILED;
+        }
+
+        if (full_state_update || state == NM_DEVICE_STATE_FAILED) {
+            nm_device_state_changed(self, state, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        return;
+    }
+
+    /* If a method is still pending but required, wait */
+    if (priv->ip_state_4 != NM_DEVICE_IP_STATE_DONE && !get_ip_config_may_fail(self, AF_INET))
+        return;
+    if (priv->ip_state_6 != NM_DEVICE_IP_STATE_DONE && !get_ip_config_may_fail(self, AF_INET6))
+        return;
+
+    /* If at least a method has completed, proceed with activation */
+    if ((priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && !ip4_disabled)
+        || (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE && !ip6_disabled)) {
+        if (full_state_update)
+            nm_device_state_changed(self, NM_DEVICE_STATE_IP_CHECK, NM_DEVICE_STATE_REASON_NONE);
+        return;
+    }
+}
+
+/**
+ * nm_device_slave_notify_enslave:
+ * @self: the slave device
+ * @success: whether the enslaving operation succeeded
+ *
+ * Notifies a slave that either it has been enslaved, or else its master tried
+ * to enslave it and failed.
+ */
+static void
+nm_device_slave_notify_enslave(NMDevice *self, gboolean success)
+{
+    NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection = nm_device_get_applied_connection(self);
+    gboolean         activating = (priv->state == NM_DEVICE_STATE_IP_CONFIG);
+
+    g_return_if_fail(priv->master);
+
+    if (!priv->is_enslaved) {
+        if (success) {
+            if (activating) {
+                _LOGI(LOGD_DEVICE,
+                      "Activation: connection '%s' enslaved, continuing activation",
+                      nm_connection_get_id(connection));
+            } else
+                _LOGI(LOGD_DEVICE, "enslaved to %s", nm_device_get_iface(priv->master));
+
+            priv->is_enslaved = TRUE;
+
+            _notify(self, PROP_MASTER);
+            _notify(priv->master, PROP_SLAVES);
+        } else if (activating) {
+            _LOGW(LOGD_DEVICE,
+                  "Activation: connection '%s' could not be enslaved",
+                  nm_connection_get_id(connection));
+        }
+    }
+
+    if (activating) {
+        if (success)
+            check_ip_state(self, FALSE, TRUE);
+        else
+            nm_device_queue_state(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_UNKNOWN);
+    } else
+        nm_device_queue_recheck_assume(self);
+}
+
+/**
+ * nm_device_slave_notify_release:
+ * @self: the slave device
+ * @reason: the reason associated with the state change
+ *
+ * Notifies a slave that it has been released, and why.
+ */
+static void
+nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason)
+{
+    NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection = nm_device_get_applied_connection(self);
+    const char *     master_status;
+
+    g_return_if_fail(priv->master);
+
+    if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state <= NM_DEVICE_STATE_ACTIVATED) {
+        switch (nm_device_state_reason_check(reason)) {
+        case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
+            master_status = "failed";
+            break;
+        case NM_DEVICE_STATE_REASON_USER_REQUESTED:
+            reason        = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED;
+            master_status = "deactivated by user request";
+            break;
+        case NM_DEVICE_STATE_REASON_CONNECTION_REMOVED:
+            reason        = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED;
+            master_status = "deactivated because master was removed";
+            break;
+        default:
+            master_status = "deactivated";
+            break;
+        }
+
+        _LOGD(LOGD_DEVICE,
+              "Activation: connection '%s' master %s",
+              nm_connection_get_id(connection),
+              master_status);
+
+        /* Cancel any pending activation sources */
+        _cancel_activation(self);
+        nm_device_queue_state(self, NM_DEVICE_STATE_DEACTIVATING, reason);
+    } else
+        _LOGI(LOGD_DEVICE, "released from master device %s", nm_device_get_iface(priv->master));
+
+    if (priv->is_enslaved) {
+        priv->is_enslaved = FALSE;
+        _notify(self, PROP_MASTER);
+        _notify(priv->master, PROP_SLAVES);
+    }
+}
+
+/**
+ * nm_device_removed:
+ * @self: the #NMDevice
+ * @unconfigure_ip_config: whether to clear the IP config objects
+ *   of the device (provided, it is still not cleared at this point).
+ *
+ * Called by the manager when the device was removed. Releases the device from
+ * the master in case it's enslaved.
+ */
+void
+nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    if (priv->master) {
+        /* this is called when something externally messes with the slave or during shut-down.
+         * Release the slave from master, but don't touch the device. */
+        nm_device_master_release_one_slave(priv->master,
+                                           self,
+                                           FALSE,
+                                           FALSE,
+                                           NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+    }
+
+    if (unconfigure_ip_config) {
+        nm_device_set_ip_config(self, AF_INET, NULL, FALSE, NULL);
+        nm_device_set_ip_config(self, AF_INET6, NULL, FALSE, NULL);
+    } else {
+        if (priv->dhcp_data_4.client)
+            nm_dhcp_client_stop(priv->dhcp_data_4.client, FALSE);
+        if (priv->dhcp_data_6.client)
+            nm_dhcp_client_stop(priv->dhcp_data_6.client, FALSE);
+    }
+}
+
+static gboolean
+is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->carrier || priv->ignore_carrier)
+        return TRUE;
+
+    if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER))
+        return TRUE;
+
+    /* master types are always available even without carrier. */
+    if (nm_device_is_master(self))
+        return TRUE;
+
+    return FALSE;
+}
+
+/**
+ * nm_device_is_available:
+ * @self: the #NMDevice
+ * @flags: additional flags to influence the check. Flags have the
+ *   meaning to increase the availability of a device.
+ *
+ * Checks if @self would currently be capable of activating a
+ * connection. In particular, it checks that the device is ready (eg,
+ * is not missing firmware), that it has carrier (if necessary), and
+ * that any necessary external software (eg, ModemManager,
+ * wpa_supplicant) is available.
+ *
+ * @self can only be in a state higher than
+ * %NM_DEVICE_STATE_UNAVAILABLE when nm_device_is_available() returns
+ * %TRUE. (But note that it can still be %NM_DEVICE_STATE_UNMANAGED
+ * when it is available.)
+ *
+ * Returns: %TRUE or %FALSE
+ */
+gboolean
+nm_device_is_available(NMDevice *self, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->firmware_missing)
+        return FALSE;
+
+    return NM_DEVICE_GET_CLASS(self)->is_available(self, flags);
+}
+
+gboolean
+nm_device_ignore_carrier_by_default(NMDevice *self)
+{
+    /* master types ignore-carrier by default. */
+    return nm_device_is_master(self);
+}
+
+gboolean
+nm_device_get_enabled(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    if (NM_DEVICE_GET_CLASS(self)->get_enabled)
+        return NM_DEVICE_GET_CLASS(self)->get_enabled(self);
+    return TRUE;
+}
+
+void
+nm_device_set_enabled(NMDevice *self, gboolean enabled)
+{
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    if (NM_DEVICE_GET_CLASS(self)->set_enabled)
+        NM_DEVICE_GET_CLASS(self)->set_enabled(self, enabled);
+}
+
+static NM_UTILS_FLAGS2STR_DEFINE(_autoconnect_blocked_flags_to_string,
+                                 NMDeviceAutoconnectBlockedFlags,
+                                 NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_NONE, "none"),
+                                 NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_USER, "user"),
+                                 NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN,
+                                                    "wrong-pin"),
+                                 NM_UTILS_FLAGS2STR(NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT,
+                                                    "manual-disconnect"), );
+
+NMDeviceAutoconnectBlockedFlags
+nm_device_autoconnect_blocked_get(NMDevice *self, NMDeviceAutoconnectBlockedFlags mask)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    if (mask == 0)
+        mask = NM_DEVICE_AUTOCONNECT_BLOCKED_ALL;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    return priv->autoconnect_blocked_flags & mask;
+}
+
+void
+nm_device_autoconnect_blocked_set_full(NMDevice *                      self,
+                                       NMDeviceAutoconnectBlockedFlags mask,
+                                       NMDeviceAutoconnectBlockedFlags value)
+{
+    NMDevicePrivate *priv;
+    gboolean         changed;
+    char             buf1[128], buf2[128];
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    nm_assert(mask);
+    nm_assert(!NM_FLAGS_ANY(mask, ~NM_DEVICE_AUTOCONNECT_BLOCKED_ALL));
+    nm_assert(!NM_FLAGS_ANY(value, ~mask));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    value = (priv->autoconnect_blocked_flags & ~mask) | (mask & value);
+    if (value == priv->autoconnect_blocked_flags)
+        return;
+
+    changed = ((!value) != (!priv->autoconnect_blocked_flags));
+
+    _LOGT(
+        LOGD_DEVICE,
+        "autoconnect-blocked: set \"%s\" (was \"%s\")",
+        _autoconnect_blocked_flags_to_string(value, buf1, sizeof(buf1)),
+        _autoconnect_blocked_flags_to_string(priv->autoconnect_blocked_flags, buf2, sizeof(buf2)));
+
+    priv->autoconnect_blocked_flags = value;
+    nm_assert(priv->autoconnect_blocked_flags == value);
+    if (changed)
+        _notify(self, PROP_AUTOCONNECT);
+}
+
+static gboolean
+autoconnect_allowed_accumulator(GSignalInvocationHint *ihint,
+                                GValue *               return_accu,
+                                const GValue *         handler_return,
+                                gpointer               data)
+{
+    if (!g_value_get_boolean(handler_return))
+        g_value_set_boolean(return_accu, FALSE);
+    return TRUE;
+}
+
+/**
+ * nm_device_autoconnect_allowed:
+ * @self: the #NMDevice
+ *
+ * Returns: %TRUE if the device can be auto-connected immediately, taking
+ * transient conditions into account (like companion devices that may wish to
+ * block autoconnect for a time).
+ */
+gboolean
+nm_device_autoconnect_allowed(NMDevice *self)
+{
+    NMDevicePrivate *priv     = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceClass *  klass    = NM_DEVICE_GET_CLASS(self);
+    GValue           instance = G_VALUE_INIT;
+    GValue           retval   = G_VALUE_INIT;
+
+    if (nm_device_autoconnect_blocked_get(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL))
+        return FALSE;
+
+    if (klass->get_autoconnect_allowed && !klass->get_autoconnect_allowed(self))
+        return FALSE;
+
+    if (!nm_device_get_enabled(self))
+        return FALSE;
+
+    if (nm_device_is_real(self)) {
+        if (priv->state < NM_DEVICE_STATE_DISCONNECTED)
+            return FALSE;
+    } else {
+        if (!nm_device_check_unrealized_device_managed(self))
+            return FALSE;
+    }
+
+    if (priv->delete_on_deactivate_data)
+        return FALSE;
+
+    /* The 'autoconnect-allowed' signal is emitted on a device to allow
+     * other listeners to block autoconnect on the device if they wish.
+     * This is mainly used by the OLPC Mesh devices to block autoconnect
+     * on their companion Wi-Fi device as they share radio resources and
+     * cannot be connected at the same time.
+     */
+
+    g_value_init(&instance, G_TYPE_OBJECT);
+    g_value_set_object(&instance, self);
+
+    g_value_init(&retval, G_TYPE_BOOLEAN);
+    g_value_set_boolean(&retval, TRUE);
+
+    /* Use g_signal_emitv() rather than g_signal_emit() to avoid the return
+     * value being changed if no handlers are connected */
+    g_signal_emitv(&instance, signals[AUTOCONNECT_ALLOWED], 0, &retval);
+    g_value_unset(&instance);
+
+    return g_value_get_boolean(&retval);
+}
+
+static gboolean
+can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    nm_assert(!specific_object || !*specific_object);
+    return TRUE;
+}
+
+/**
+ * nm_device_can_auto_connect:
+ * @self: an #NMDevice
+ * @sett_conn: a #NMSettingsConnection
+ * @specific_object: (out) (transfer full): on output, the path of an
+ *   object associated with the returned connection, to be passed to
+ *   nm_manager_activate_connection(), or %NULL.
+ *
+ * Checks if @sett_conn can be auto-activated on @self right now.
+ * This requires, at a minimum, that the connection be compatible with
+ * @self, and that it have the #NMSettingConnection:autoconnect property
+ * set, and that the device allow auto connections. Some devices impose
+ * additional requirements. (Eg, a Wi-Fi connection can only be activated
+ * if its SSID was seen in the last scan.)
+ *
+ * Returns: %TRUE, if the @sett_conn can be auto-activated.
+ **/
+gboolean
+nm_device_can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(sett_conn), FALSE);
+    g_return_val_if_fail(!specific_object || !*specific_object, FALSE);
+
+    /* the caller must ensure that nm_device_autoconnect_allowed() returns
+     * TRUE as well. This is done, because nm_device_can_auto_connect()
+     * has only one caller, and it iterates over a list of available
+     * connections.
+     *
+     * Hence, we don't need to re-check nm_device_autoconnect_allowed()
+     * over and over again. The caller is supposed to do that. */
+    nm_assert(nm_device_autoconnect_allowed(self));
+
+    if (!nm_device_check_connection_available(self,
+                                              nm_settings_connection_get_connection(sett_conn),
+                                              NM_DEVICE_CHECK_CON_AVAILABLE_NONE,
+                                              NULL,
+                                              NULL))
+        return FALSE;
+
+    if (!NM_DEVICE_GET_CLASS(self)->can_auto_connect(self, sett_conn, specific_object))
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+device_has_config(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* Check for IP configuration. */
+    if (priv->ip_config_4 && nm_ip4_config_get_num_addresses(priv->ip_config_4))
+        return TRUE;
+    if (priv->ip_config_6 && nm_ip6_config_get_num_addresses(priv->ip_config_6))
+        return TRUE;
+
+    /* The existence of a software device is good enough. */
+    if (nm_device_is_software(self) && nm_device_is_real(self))
+        return TRUE;
+
+    /* Master-slave relationship is also a configuration */
+    if (!c_list_is_empty(&priv->slaves)
+        || nm_platform_link_get_master(nm_device_get_platform(self), priv->ifindex) > 0)
+        return TRUE;
+
+    return FALSE;
+}
+
+/**
+ * nm_device_master_update_slave_connection:
+ * @self: the master #NMDevice
+ * @slave: the slave #NMDevice
+ * @connection: the #NMConnection to update with the slave settings
+ * @GError: (out): error description
+ *
+ * Reads the slave configuration for @slave and updates @connection with those
+ * properties. This invokes a virtual function on the master device @self.
+ *
+ * Returns: %TRUE if the configuration was read and @connection updated,
+ * %FALSE on failure.
+ */
+gboolean
+nm_device_master_update_slave_connection(NMDevice *    self,
+                                         NMDevice *    slave,
+                                         NMConnection *connection,
+                                         GError **     error)
+{
+    NMDeviceClass *klass;
+    gboolean       success;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(slave, FALSE);
+    g_return_val_if_fail(connection, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+    g_return_val_if_fail(nm_connection_get_setting_connection(connection), FALSE);
+
+    g_return_val_if_fail(nm_device_get_iface(self), FALSE);
+
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (klass->master_update_slave_connection) {
+        success = klass->master_update_slave_connection(self, slave, connection, error);
+
+        g_return_val_if_fail(!error || (success && !*error) || *error, success);
+        return success;
+    }
+
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_FAILED,
+                "master device '%s' cannot update a slave connection for slave device '%s' (master "
+                "type not supported?)",
+                nm_device_get_iface(self),
+                nm_device_get_iface(slave));
+    return FALSE;
+}
+
+static gboolean
+_get_maybe_ipv6_disabled(NMDevice *self)
+{
+    NMPlatform *platform;
+    int         ifindex;
+    const char *path;
+    char        ifname[IFNAMSIZ];
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        return FALSE;
+
+    platform = nm_device_get_platform(self);
+    if (!nm_platform_if_indextoname(platform, ifindex, ifname))
+        return FALSE;
+
+    path = nm_sprintf_bufa(128, "/proc/sys/net/ipv6/conf/%s/disable_ipv6", ifname);
+    return (nm_platform_sysctl_get_int32(platform, NMP_SYSCTL_PATHID_ABSOLUTE(path), 0) == 0);
+}
+
+NMConnection *
+nm_device_generate_connection(NMDevice *self,
+                              NMDevice *master,
+                              gboolean *out_maybe_later,
+                              GError ** error)
+{
+    NMDeviceClass *  klass                   = NM_DEVICE_GET_CLASS(self);
+    NMDevicePrivate *priv                    = NM_DEVICE_GET_PRIVATE(self);
+    const char *     ifname                  = nm_device_get_iface(self);
+    gs_unref_object NMConnection *connection = NULL;
+    NMSetting *                   s_con;
+    NMSetting *                   s_ip4;
+    NMSetting *                   s_ip6;
+    char                          uuid[37];
+    const char *                  ip4_method, *ip6_method;
+    GError *                      local = NULL;
+    const NMPlatformLink *        pllink;
+
+    NM_SET_OUT(out_maybe_later, FALSE);
+
+    /* If update_connection() is not implemented, just fail. */
+    if (!klass->update_connection) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "device class %s does not support generating a connection",
+                    G_OBJECT_TYPE_NAME(self));
+        return NULL;
+    }
+
+    /* Return NULL if device is unconfigured. */
+    if (!device_has_config(self)) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "device has no existing configuration");
+        return NULL;
+    }
+
+    connection = nm_simple_connection_new();
+    s_con      = nm_setting_connection_new();
+
+    g_object_set(s_con,
+                 NM_SETTING_CONNECTION_UUID,
+                 nm_utils_uuid_generate_buf(uuid),
+                 NM_SETTING_CONNECTION_ID,
+                 ifname,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 FALSE,
+                 NM_SETTING_CONNECTION_INTERFACE_NAME,
+                 ifname,
+                 NM_SETTING_CONNECTION_TIMESTAMP,
+                 (guint64) time(NULL),
+                 NULL);
+
+    if (klass->connection_type_supported)
+        g_object_set(s_con, NM_SETTING_CONNECTION_TYPE, klass->connection_type_supported, NULL);
+
+    nm_connection_add_setting(connection, s_con);
+
+    /* If the device is a slave, update various slave settings */
+    if (master) {
+        if (!nm_device_master_update_slave_connection(master, self, connection, &local)) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "master device '%s' failed to update slave connection: %s",
+                        nm_device_get_iface(master),
+                        local->message);
+            g_error_free(local);
+            return NULL;
+        }
+    } else {
+        /* Only regular and master devices get IP configuration; slaves do not */
+        s_ip4 = nm_ip4_config_create_setting(priv->ip_config_4);
+        nm_connection_add_setting(connection, s_ip4);
+
+        s_ip6 = nm_ip6_config_create_setting(priv->ip_config_6, _get_maybe_ipv6_disabled(self));
+        nm_connection_add_setting(connection, s_ip6);
+
+        nm_connection_add_setting(connection, nm_setting_proxy_new());
+
+        pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ifindex);
+        if (pllink && pllink->inet6_token.id) {
+            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+            g_object_set(s_ip6,
+                         NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE,
+                         NM_IN6_ADDR_GEN_MODE_EUI64,
+                         NM_SETTING_IP6_CONFIG_TOKEN,
+                         nm_utils_inet6_interface_identifier_to_token(pllink->inet6_token, sbuf),
+                         NULL);
+        }
+    }
+
+    klass->update_connection(self, connection);
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &local)) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "generated connection does not verify: %s",
+                    local->message);
+        g_error_free(local);
+        return NULL;
+    }
+
+    /* Ignore the connection if it has no IP configuration,
+     * no slave configuration, and is not a master interface.
+     */
+    ip4_method = nm_utils_get_ip_config_method(connection, AF_INET);
+    ip6_method = nm_utils_get_ip_config_method(connection, AF_INET6);
+    if (nm_streq0(ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)
+        && NM_IN_STRSET(ip6_method,
+                        NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                        NM_SETTING_IP6_CONFIG_METHOD_DISABLED)
+        && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con))
+        && c_list_is_empty(&priv->slaves)) {
+        NM_SET_OUT(out_maybe_later, TRUE);
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_FAILED,
+            "ignoring generated connection (no IP and not in master-slave relationship)");
+        return NULL;
+    }
+
+    /* Ignore any IPv6LL-only, not master connections without slaves,
+     * unless they are in the assume-ipv6ll-only list.
+     */
+    if (nm_streq0(ip4_method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)
+        && nm_streq0(ip6_method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)
+        && !nm_setting_connection_get_master(NM_SETTING_CONNECTION(s_con))
+        && c_list_is_empty(&priv->slaves)
+        && !nm_config_data_get_assume_ipv6ll_only(NM_CONFIG_GET_DATA, self)) {
+        _LOGD(LOGD_DEVICE,
+              "ignoring generated connection (IPv6LL-only and not in master-slave relationship)");
+        NM_SET_OUT(out_maybe_later, TRUE);
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_FAILED,
+            "ignoring generated connection (IPv6LL-only and not in master-slave relationship)");
+        return NULL;
+    }
+
+    return g_steal_pointer(&connection);
+}
+
+/**
+ * nm_device_complete_connection:
+ *
+ * Complete the connection. This is solely used for AddAndActivate where the user
+ * may pass in an incomplete connection and a device, and the device tries to
+ * make sense of it and complete it for activation. Otherwise, this is not
+ * used.
+ *
+ * Returns: success or failure.
+ */
+gboolean
+nm_device_complete_connection(NMDevice *           self,
+                              NMConnection *       connection,
+                              const char *         specific_object,
+                              NMConnection *const *existing_connections,
+                              GError **            error)
+{
+    NMDeviceClass *klass;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
+
+    klass = NM_DEVICE_GET_CLASS(self);
+
+    if (!klass->complete_connection) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_CONNECTION,
+                    "Device class %s had no complete_connection method",
+                    G_OBJECT_TYPE_NAME(self));
+        return FALSE;
+    }
+
+    if (!klass->complete_connection(self, connection, specific_object, existing_connections, error))
+        return FALSE;
+
+    if (!nm_connection_normalize(connection, NULL, NULL, error))
+        return FALSE;
+
+    return nm_device_check_connection_compatible(self, connection, error);
+}
+
+gboolean
+nm_device_match_parent(NMDevice *self, const char *parent)
+{
+    NMDevice *parent_device;
+
+    g_return_val_if_fail(parent, FALSE);
+
+    parent_device = nm_device_parent_get_device(self);
+    if (!parent_device)
+        return FALSE;
+
+    if (nm_utils_is_uuid(parent)) {
+        NMConnection *connection;
+
+        /* If the parent is a UUID, the connection matches when there is
+         * no connection active on the device or when a connection with
+         * that UUID is active.
+         */
+        connection = nm_device_get_applied_connection(parent_device);
+        if (connection && !nm_streq0(parent, nm_connection_get_uuid(connection)))
+            return FALSE;
+    } else {
+        /* Interface name */
+        if (!nm_streq0(parent, nm_device_get_ip_iface(parent_device)))
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+nm_device_match_parent_hwaddr(NMDevice *    device,
+                              NMConnection *connection,
+                              gboolean      fail_if_no_hwaddr)
+{
+    NMSettingWired *s_wired;
+    NMDevice *      parent_device;
+    const char *    setting_mac;
+    const char *    parent_mac;
+
+    s_wired = nm_connection_get_setting_wired(connection);
+    if (!s_wired)
+        return !fail_if_no_hwaddr;
+
+    setting_mac = nm_setting_wired_get_mac_address(s_wired);
+    if (!setting_mac)
+        return !fail_if_no_hwaddr;
+
+    parent_device = nm_device_parent_get_device(device);
+    if (!parent_device)
+        return !fail_if_no_hwaddr;
+
+    parent_mac = nm_device_get_permanent_hw_address(parent_device);
+    return parent_mac && nm_utils_hwaddr_matches(setting_mac, -1, parent_mac, -1);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error)
+{
+    NMDevicePrivate *priv            = NM_DEVICE_GET_PRIVATE(self);
+    const char *     device_iface    = nm_device_get_iface(self);
+    gs_free_error GError *local      = NULL;
+    gs_free char *        conn_iface = NULL;
+    NMDeviceClass *       klass;
+    NMSettingMatch *      s_match;
+
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (klass->connection_type_check_compatible) {
+        if (!_nm_connection_check_main_setting(connection,
+                                               klass->connection_type_check_compatible,
+                                               error))
+            return FALSE;
+    } else if (klass->check_connection_compatible == check_connection_compatible) {
+        /* the device class does not implement check_connection_compatible nor set
+         * connection_type_check_compatible. That means, it is by default not compatible
+         * with any connection type. */
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                   "device does not support any connections");
+        return FALSE;
+    }
+
+    conn_iface = nm_manager_get_connection_iface(NM_MANAGER_GET, connection, NULL, NULL, &local);
+
+    /* We always need a interface name for virtual devices, but for
+     * physical ones a connection without interface name is fine for
+     * any device. */
+    if (!conn_iface) {
+        if (nm_connection_is_virtual(connection)) {
+            nm_utils_error_set(error,
+                               NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                               "cannot get interface name due to %s",
+                               local->message);
+            return FALSE;
+        }
+    } else if (!nm_streq0(conn_iface, device_iface)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "mismatching interface name");
+        return FALSE;
+    }
+
+    s_match = (NMSettingMatch *) nm_connection_get_setting(connection, NM_TYPE_SETTING_MATCH);
+    if (s_match) {
+        const char *const *patterns;
+        const char *       device_driver;
+        guint              num_patterns = 0;
+
+        patterns = nm_setting_match_get_interface_names(s_match, &num_patterns);
+        if (!nm_wildcard_match_check(device_iface, patterns, num_patterns)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "device does not satisfy match.interface-name property");
+            return FALSE;
+        }
+
+        patterns = nm_setting_match_get_kernel_command_lines(s_match, &num_patterns);
+        if (num_patterns > 0
+            && !nm_utils_kernel_cmdline_match_check(nm_utils_proc_cmdline_split(),
+                                                    patterns,
+                                                    num_patterns,
+                                                    error))
+            return FALSE;
+
+        device_driver = nm_device_get_driver(self);
+        patterns      = nm_setting_match_get_drivers(s_match, &num_patterns);
+        if (!nm_wildcard_match_check(device_driver, patterns, num_patterns)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "device does not satisfy match.driver property");
+            return FALSE;
+        }
+
+        patterns = nm_setting_match_get_paths(s_match, &num_patterns);
+        if (!nm_wildcard_match_check(priv->path, patterns, num_patterns)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "device does not satisfy match.path property");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/**
+ * nm_device_check_connection_compatible:
+ * @self: an #NMDevice
+ * @connection: an #NMConnection
+ * @error: optional reason why it is incompatible. Note that the
+ *   error code is set to %NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+ *   if the profile is fundamentally incompatible with the device
+ *   (most commonly, because the device-type does not support the
+ *   connection-type).
+ *
+ * Checks if @connection could potentially be activated on @self.
+ * This means only that @self has the proper capabilities, and that
+ * @connection is not locked to some other device. It does not
+ * necessarily mean that @connection could be activated on @self
+ * right now. (Eg, it might refer to a Wi-Fi network that is not
+ * currently available.)
+ *
+ * Returns: #TRUE if @connection could potentially be activated on
+ *   @self.
+ */
+gboolean
+nm_device_check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
+
+    return NM_DEVICE_GET_CLASS(self)->check_connection_compatible(self, connection, error);
+}
+
+gboolean
+nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave)
+{
+    NMSettingConnection *s_con;
+    const char *         connection_type, *slave_type;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(slave), FALSE);
+
+    if (!nm_device_is_master(self))
+        return FALSE;
+
+    /* All masters should have connection type set */
+    connection_type = NM_DEVICE_GET_CLASS(self)->connection_type_supported;
+    g_return_val_if_fail(connection_type, FALSE);
+
+    s_con = nm_connection_get_setting_connection(slave);
+    g_assert(s_con);
+    slave_type = nm_setting_connection_get_slave_type(s_con);
+    if (!slave_type)
+        return FALSE;
+
+    return nm_streq(connection_type, slave_type);
+}
+
+/**
+ * nm_device_can_assume_connections:
+ * @self: #NMDevice instance
+ *
+ * This is a convenience function to determine whether connection assumption
+ * is available for this device.
+ *
+ * Returns: %TRUE if the device is capable of assuming connections, %FALSE if not
+ */
+static gboolean
+nm_device_can_assume_connections(NMDevice *self)
+{
+    return !!NM_DEVICE_GET_CLASS(self)->update_connection;
+}
+
+static gboolean
+unmanaged_on_quit(NMDevice *self)
+{
+    NMConnection *connection;
+
+    /* NMDeviceWifi overwrites this function to always unmanage wifi devices.
+     *
+     * For all other types, if the device type can assume connections, we leave
+     * it up on quit.
+     *
+     * Originally, we would only keep devices up that can be assumed afterwards.
+     * However, that meant we unmanged layer-2 only devices. So, this was step
+     * by step refined to unmanage less (commit 25aaaab3, rh#1311988, rh#1333983).
+     * But there are more scenarios where we also want to keep the device up
+     * (rh#1378418, rh#1371126). */
+    if (!nm_device_can_assume_connections(self))
+        return TRUE;
+
+    /* the only exception are IPv4 shared connections. We unmanage them on quit. */
+    connection = nm_device_get_applied_connection(self);
+    if (connection) {
+        if (NM_IN_STRSET(nm_utils_get_ip_config_method(connection, AF_INET),
+                         NM_SETTING_IP4_CONFIG_METHOD_SHARED)) {
+            /* shared connections are to be unmangaed. */
+            return TRUE;
+        }
+    }
+
+    return FALSE;
+}
+
+gboolean
+nm_device_unmanage_on_quit(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    return NM_DEVICE_GET_CLASS(self)->unmanaged_on_quit(self);
+}
+
+static gboolean
+nm_device_emit_recheck_assume(gpointer user_data)
+{
+    NMDevice *       self = user_data;
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->recheck_assume_id = 0;
+    if (!nm_device_get_act_request(self))
+        g_signal_emit(self, signals[RECHECK_ASSUME], 0);
+
+    return G_SOURCE_REMOVE;
+}
+
+void
+nm_device_queue_recheck_assume(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->recheck_assume_id && nm_device_can_assume_connections(self))
+        priv->recheck_assume_id = g_idle_add(nm_device_emit_recheck_assume, self);
+}
+
+static gboolean
+recheck_available(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         now_available;
+    NMDeviceState    state     = nm_device_get_state(self);
+    NMDeviceState    new_state = NM_DEVICE_STATE_UNKNOWN;
+
+    priv->recheck_available.call_id = 0;
+
+    now_available = nm_device_is_available(self, NM_DEVICE_CHECK_DEV_AVAILABLE_NONE);
+
+    if (state == NM_DEVICE_STATE_UNAVAILABLE && now_available) {
+        new_state = NM_DEVICE_STATE_DISCONNECTED;
+        nm_device_queue_state(self, new_state, priv->recheck_available.available_reason);
+    } else if (state >= NM_DEVICE_STATE_DISCONNECTED && !now_available) {
+        new_state = NM_DEVICE_STATE_UNAVAILABLE;
+        nm_device_queue_state(self, new_state, priv->recheck_available.unavailable_reason);
+    }
+
+    if (new_state > NM_DEVICE_STATE_UNKNOWN) {
+        _LOGD(LOGD_DEVICE,
+              "is %savailable, %s %s",
+              now_available ? "" : "not ",
+              new_state == NM_DEVICE_STATE_UNAVAILABLE ? "no change required for"
+                                                       : "will transition to",
+              nm_device_state_to_str(new_state == NM_DEVICE_STATE_UNAVAILABLE ? state : new_state));
+
+        priv->recheck_available.available_reason   = NM_DEVICE_STATE_REASON_NONE;
+        priv->recheck_available.unavailable_reason = NM_DEVICE_STATE_REASON_NONE;
+    }
+
+    if (priv->recheck_available.call_id == 0)
+        nm_device_remove_pending_action(self, NM_PENDING_ACTION_RECHECK_AVAILABLE, TRUE);
+
+    return G_SOURCE_REMOVE;
+}
+
+void
+nm_device_queue_recheck_available(NMDevice *          self,
+                                  NMDeviceStateReason available_reason,
+                                  NMDeviceStateReason unavailable_reason)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->recheck_available.available_reason   = available_reason;
+    priv->recheck_available.unavailable_reason = unavailable_reason;
+    if (!priv->recheck_available.call_id) {
+        priv->recheck_available.call_id = g_idle_add(recheck_available, self);
+        nm_device_add_pending_action (self, NM_PENDING_ACTION_RECHECK_AVAILABLE,
+                                      FALSE /* cannot assert, because of how recheck_available() first clears
+                                             * the call-id and postpones removing the pending-action. */);
+    }
+}
+
+void
+nm_device_emit_recheck_auto_activate(NMDevice *self)
+{
+    g_signal_emit(self, signals[RECHECK_AUTO_ACTIVATE], 0);
+}
+
+static void
+dnsmasq_state_changed_cb(NMDnsMasqManager *manager, guint32 status, gpointer user_data)
+{
+    NMDevice *self = NM_DEVICE(user_data);
+
+    switch (status) {
+    case NM_DNSMASQ_STATUS_DEAD:
+        nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_SHARED_START_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+void
+nm_device_auth_request(NMDevice *                     self,
+                       GDBusMethodInvocation *        context,
+                       NMConnection *                 connection,
+                       const char *                   permission,
+                       gboolean                       allow_interaction,
+                       GCancellable *                 cancellable,
+                       NMManagerDeviceAuthRequestFunc callback,
+                       gpointer                       user_data)
+{
+    nm_manager_device_auth_request(nm_device_get_manager(self),
+                                   self,
+                                   context,
+                                   connection,
+                                   permission,
+                                   allow_interaction,
+                                   cancellable,
+                                   callback,
+                                   user_data);
+}
+
+/*****************************************************************************/
+
+static void
+activation_source_clear(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (priv->activation_source_id_x[IS_IPv4] != 0) {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: clear %s,v%c (id %u)",
+              _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]),
+              nm_utils_addr_family_to_char(addr_family),
+              priv->activation_source_id_x[IS_IPv4]);
+        nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]);
+        priv->activation_source_func_x[IS_IPv4] = NULL;
+    }
+}
+
+static gboolean
+activation_source_handle_cb(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *    priv;
+    const int            IS_IPv4 = NM_IS_IPv4(addr_family);
+    ActivationHandleFunc activation_source_func;
+    guint                activation_source_id;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    activation_source_func = priv->activation_source_func_x[IS_IPv4];
+    activation_source_id   = priv->activation_source_id_x[IS_IPv4];
+
+    g_return_val_if_fail(activation_source_id != 0, G_SOURCE_REMOVE);
+    nm_assert(activation_source_func);
+
+    priv->activation_source_func_x[IS_IPv4] = NULL;
+    priv->activation_source_id_x[IS_IPv4]   = 0;
+
+    _LOGD(LOGD_DEVICE,
+          "activation-stage: invoke %s,v%c (id %u)",
+          _activation_func_to_string(activation_source_func),
+          nm_utils_addr_family_to_char(addr_family),
+          activation_source_id);
+
+    activation_source_func(self);
+
+    _LOGT(LOGD_DEVICE,
+          "activation-stage: complete %s,v%c (id %u)",
+          _activation_func_to_string(activation_source_func),
+          nm_utils_addr_family_to_char(addr_family),
+          activation_source_id);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+activation_source_handle_cb_4(gpointer user_data)
+{
+    return activation_source_handle_cb(user_data, AF_INET);
+}
+
+static gboolean
+activation_source_handle_cb_6(gpointer user_data)
+{
+    return activation_source_handle_cb(user_data, AF_INET6);
+}
+
+static void
+activation_source_schedule(NMDevice *self, ActivationHandleFunc func, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    guint            new_id  = 0;
+
+    if (priv->activation_source_id_x[IS_IPv4] != 0
+        && priv->activation_source_func_x[IS_IPv4] == func) {
+        /* Scheduling the same stage multiple times is fine. */
+        _LOGT(LOGD_DEVICE,
+              "activation-stage: already scheduled %s,v%c (id %u)",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family),
+              priv->activation_source_id_x[IS_IPv4]);
+        return;
+    }
+
+    new_id =
+        g_idle_add(IS_IPv4 ? activation_source_handle_cb_4 : activation_source_handle_cb_6, self);
+
+    if (priv->activation_source_id_x[IS_IPv4] != 0) {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: schedule %s,v%c which replaces %s,v%c (id %u -> %u)",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family),
+              _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]),
+              nm_utils_addr_family_to_char(addr_family),
+              priv->activation_source_id_x[IS_IPv4],
+              new_id);
+        nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]);
+    } else {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: schedule %s,v%c (id %u)",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family),
+              new_id);
+    }
+
+    priv->activation_source_func_x[IS_IPv4] = func;
+    priv->activation_source_id_x[IS_IPv4]   = new_id;
+}
+
+static void
+activation_source_invoke_sync(NMDevice *self, ActivationHandleFunc func, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (priv->activation_source_id_x[IS_IPv4] == 0) {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: synchronously invoke %s,v%c",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family));
+    } else if (priv->activation_source_func_x[IS_IPv4] == func) {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: synchronously invoke %s,v%c which was already scheduled (id %u)",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family),
+              priv->activation_source_id_x[IS_IPv4]);
+    } else {
+        _LOGD(LOGD_DEVICE,
+              "activation-stage: synchronously invoke %s,v%c which replaces %s,v%c (id %u)",
+              _activation_func_to_string(func),
+              nm_utils_addr_family_to_char(addr_family),
+              _activation_func_to_string(priv->activation_source_func_x[IS_IPv4]),
+              nm_utils_addr_family_to_char(addr_family),
+              priv->activation_source_id_x[IS_IPv4]);
+    }
+
+    nm_clear_g_source(&priv->activation_source_id_x[IS_IPv4]);
+    priv->activation_source_func_x[IS_IPv4] = NULL;
+
+    func(self);
+}
+
+/*****************************************************************************/
+
+static void
+master_ready(NMDevice *self, NMActiveConnection *active)
+{
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMActiveConnection *master_connection;
+    NMDevice *          master;
+
+    /* Notify a master device that it has a new slave */
+    nm_assert(nm_active_connection_get_master_ready(active));
+
+    master_connection = nm_active_connection_get_master(active);
+
+    master = nm_active_connection_get_device(master_connection);
+
+    _LOGD(LOGD_DEVICE, "master connection ready; master device %s", nm_device_get_iface(master));
+
+    if (priv->master && priv->master != master)
+        nm_device_master_release_one_slave(priv->master,
+                                           self,
+                                           FALSE,
+                                           FALSE,
+                                           NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+
+    /* If the master didn't change, add-slave only rechecks whether to assume a connection. */
+    nm_device_master_add_slave(master,
+                               self,
+                               !nm_device_sys_iface_state_is_external_or_assume(self));
+}
+
+static void
+master_ready_cb(NMActiveConnection *active, GParamSpec *pspec, NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(nm_active_connection_get_master_ready(active));
+
+    if (priv->state == NM_DEVICE_STATE_PREPARE)
+        nm_device_activate_schedule_stage1_device_prepare(self, FALSE);
+}
+
+static void
+lldp_neighbors_changed(NMLldpListener *lldp_listener, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *self = NM_DEVICE(user_data);
+
+    _notify(self, PROP_LLDP_NEIGHBORS);
+}
+
+static NMPlatformVF *
+sriov_vf_config_to_platform(NMDevice *self, NMSriovVF *vf, GError **error)
+{
+    NMDevicePrivate *priv         = NM_DEVICE_GET_PRIVATE(self);
+    gs_free NMPlatformVF *plat_vf = NULL;
+    const guint *         vlan_ids;
+    GVariant *            variant;
+    guint                 i, num_vlans;
+    gsize                 length;
+
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    vlan_ids = nm_sriov_vf_get_vlan_ids(vf, &num_vlans);
+    plat_vf  = g_malloc0(sizeof(NMPlatformVF) + sizeof(NMPlatformVFVlan) * num_vlans);
+
+    plat_vf->index = nm_sriov_vf_get_index(vf);
+
+    variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_SPOOF_CHECK);
+    if (variant)
+        plat_vf->spoofchk = g_variant_get_boolean(variant);
+    else
+        plat_vf->spoofchk = -1;
+
+    variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_TRUST);
+    if (variant)
+        plat_vf->trust = g_variant_get_boolean(variant);
+    else
+        plat_vf->trust = -1;
+
+    variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MAC);
+    if (variant) {
+        if (!_nm_utils_hwaddr_aton(g_variant_get_string(variant, NULL),
+                                   plat_vf->mac.data,
+                                   sizeof(plat_vf->mac.data),
+                                   &length)) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "invalid MAC %s",
+                        g_variant_get_string(variant, NULL));
+            return NULL;
+        }
+        if (length != priv->hw_addr_len) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "wrong MAC length %" G_GSIZE_FORMAT ", should be %u",
+                        length,
+                        priv->hw_addr_len);
+            return NULL;
+        }
+        plat_vf->mac.len = length;
+    }
+
+    variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MIN_TX_RATE);
+    if (variant)
+        plat_vf->min_tx_rate = g_variant_get_uint32(variant);
+
+    variant = nm_sriov_vf_get_attribute(vf, NM_SRIOV_VF_ATTRIBUTE_MAX_TX_RATE);
+    if (variant)
+        plat_vf->max_tx_rate = g_variant_get_uint32(variant);
+
+    plat_vf->num_vlans = num_vlans;
+    plat_vf->vlans     = (NMPlatformVFVlan *) (&plat_vf[1]);
+    for (i = 0; i < num_vlans; i++) {
+        plat_vf->vlans[i].id  = vlan_ids[i];
+        plat_vf->vlans[i].qos = nm_sriov_vf_get_vlan_qos(vf, vlan_ids[i]);
+        plat_vf->vlans[i].proto_ad =
+            nm_sriov_vf_get_vlan_protocol(vf, vlan_ids[i]) == NM_SRIOV_VF_VLAN_PROTOCOL_802_1AD;
+    }
+
+    return g_steal_pointer(&plat_vf);
+}
+
+static void
+sriov_params_cb(GError *error, gpointer user_data)
+{
+    NMDevice *       self;
+    NMDevicePrivate *priv;
+    nm_auto_freev NMPlatformVF **plat_vfs = NULL;
+
+    nm_utils_user_data_unpack(user_data, &self, &plat_vfs);
+
+    if (nm_utils_error_is_cancelled_or_disposing(error))
+        return;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (error) {
+        _LOGE(LOGD_DEVICE, "failed to set SR-IOV parameters: %s", error->message);
+        nm_device_state_changed(self,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED);
+        return;
+    }
+
+    if (!nm_platform_link_set_sriov_vfs(nm_device_get_platform(self),
+                                        priv->ifindex,
+                                        (const NMPlatformVF *const *) plat_vfs)) {
+        _LOGE(LOGD_DEVICE, "failed to apply SR-IOV VFs");
+        nm_device_state_changed(self,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED);
+        return;
+    }
+
+    priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+
+    nm_device_activate_schedule_stage1_device_prepare(self, FALSE);
+}
+
+/*
+ * activate_stage1_device_prepare
+ *
+ * Prepare for device activation
+ *
+ */
+static void
+activate_stage1_device_prepare(NMDevice *self)
+{
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMActStageReturn    ret  = NM_ACT_STAGE_RETURN_SUCCESS;
+    NMActiveConnection *active;
+    NMActiveConnection *master;
+    NMDeviceClass *     klass;
+
+    priv->v4_route_table_initialized = FALSE;
+    priv->v6_route_table_initialized = FALSE;
+
+    _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_NONE);
+    _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_NONE);
+
+    /* Notify the new ActiveConnection along with the state change */
+    nm_dbus_track_obj_path_set(&priv->act_request, priv->act_request.obj, TRUE);
+
+    nm_device_state_changed(self, NM_DEVICE_STATE_PREPARE, NM_DEVICE_STATE_REASON_NONE);
+
+    if (priv->stage1_sriov_state != NM_DEVICE_STAGE_STATE_COMPLETED) {
+        NMSettingSriov *s_sriov;
+
+        if (nm_device_sys_iface_state_is_external_or_assume(self)) {
+            /* pass */
+        } else if (priv->stage1_sriov_state == NM_DEVICE_STAGE_STATE_PENDING)
+            return;
+        else if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)
+                 && (s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))) {
+            nm_auto_freev NMPlatformVF **plat_vfs = NULL;
+            gs_free_error GError *error           = NULL;
+            NMSriovVF *           vf;
+            NMTernary             autoprobe;
+            guint                 num;
+            guint                 i;
+
+            autoprobe = nm_setting_sriov_get_autoprobe_drivers(s_sriov);
+            if (autoprobe == NM_TERNARY_DEFAULT) {
+                autoprobe = nm_config_data_get_connection_default_int64(
+                    NM_CONFIG_GET_DATA,
+                    NM_CON_DEFAULT("sriov.autoprobe-drivers"),
+                    self,
+                    NM_OPTION_BOOL_FALSE,
+                    NM_OPTION_BOOL_TRUE,
+                    NM_OPTION_BOOL_TRUE);
+            }
+
+            num      = nm_setting_sriov_get_num_vfs(s_sriov);
+            plat_vfs = g_new0(NMPlatformVF *, num + 1);
+            for (i = 0; i < num; i++) {
+                vf          = nm_setting_sriov_get_vf(s_sriov, i);
+                plat_vfs[i] = sriov_vf_config_to_platform(self, vf, &error);
+                if (!plat_vfs[i]) {
+                    _LOGE(LOGD_DEVICE,
+                          "failed to apply SR-IOV VF '%s': %s",
+                          nm_utils_sriov_vf_to_str(vf, FALSE, NULL),
+                          error->message);
+                    nm_device_state_changed(self,
+                                            NM_DEVICE_STATE_FAILED,
+                                            NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED);
+                    return;
+                }
+            }
+
+            /* When changing the number of VFs the kernel can block
+             * for very long time in the write to sysfs, especially
+             * if autoprobe-drivers is enabled. Do it asynchronously
+             * to avoid blocking the entire NM process.
+             */
+            sriov_op_queue(self,
+                           nm_setting_sriov_get_total_vfs(s_sriov),
+                           NM_TERNARY_TO_OPTION_BOOL(autoprobe),
+                           sriov_params_cb,
+                           nm_utils_user_data_pack(self, g_steal_pointer(&plat_vfs)));
+            priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_PENDING;
+            return;
+        }
+        priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    }
+
+    /* Assumed connections were already set up outside NetworkManager */
+    klass = NM_DEVICE_GET_CLASS(self);
+
+    if (klass->act_stage1_prepare_set_hwaddr_ethernet
+        && !nm_device_sys_iface_state_is_external_or_assume(self)) {
+        if (!nm_device_hw_addr_set_cloned(self, nm_device_get_applied_connection(self), FALSE)) {
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            return;
+        }
+    }
+
+    if (klass->act_stage1_prepare_also_for_external_or_assume
+        || !nm_device_sys_iface_state_is_external_or_assume(self)) {
+        nm_assert(!klass->act_stage1_prepare_also_for_external_or_assume
+                  || klass->act_stage1_prepare);
+        if (klass->act_stage1_prepare) {
+            NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE;
+
+            ret = klass->act_stage1_prepare(self, &failure_reason);
+            if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
+                nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
+                return;
+            }
+            if (ret == NM_ACT_STAGE_RETURN_POSTPONE)
+                return;
+
+            nm_assert(ret == NM_ACT_STAGE_RETURN_SUCCESS);
+        }
+    }
+
+    active = NM_ACTIVE_CONNECTION(priv->act_request.obj);
+    master = nm_active_connection_get_master(active);
+    if (master) {
+        if (nm_active_connection_get_state(master) >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
+            _LOGD(LOGD_DEVICE, "master connection is deactivating");
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED);
+            return;
+        }
+        /* If the master connection is ready for slaves, attach ourselves */
+        if (!nm_active_connection_get_master_ready(active)) {
+            if (priv->master_ready_id == 0) {
+                _LOGD(LOGD_DEVICE, "waiting for master connection to become ready");
+                priv->master_ready_id =
+                    g_signal_connect(active,
+                                     "notify::" NM_ACTIVE_CONNECTION_INT_MASTER_READY,
+                                     (GCallback) master_ready_cb,
+                                     self);
+            }
+            return;
+        }
+    }
+    nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id);
+    if (master)
+        master_ready(self, active);
+    else if (priv->master) {
+        nm_device_master_release_one_slave(priv->master,
+                                           self,
+                                           TRUE,
+                                           TRUE,
+                                           NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+    }
+
+    nm_device_activate_schedule_stage2_device_config(self, TRUE);
+}
+
+void
+nm_device_activate_schedule_stage1_device_prepare(NMDevice *self, gboolean do_sync)
+{
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_DEVICE_GET_PRIVATE(self)->act_request.obj);
+
+    if (!do_sync) {
+        activation_source_schedule(self, activate_stage1_device_prepare, AF_INET);
+        return;
+    }
+
+    activation_source_invoke_sync(self, activate_stage1_device_prepare, AF_INET);
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *self, NMDeviceStateReason *out_failure_reason)
+{
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+lldp_init(NMDevice *self, gboolean restart)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->ifindex > 0 && _prop_get_connection_lldp(self)) {
+        gs_free_error GError *error = NULL;
+
+        if (priv->lldp_listener) {
+            if (restart && nm_lldp_listener_is_running(priv->lldp_listener))
+                nm_lldp_listener_stop(priv->lldp_listener);
+        } else {
+            priv->lldp_listener = nm_lldp_listener_new();
+            g_signal_connect(priv->lldp_listener,
+                             "notify::" NM_LLDP_LISTENER_NEIGHBORS,
+                             G_CALLBACK(lldp_neighbors_changed),
+                             self);
+        }
+
+        if (!nm_lldp_listener_is_running(priv->lldp_listener)) {
+            if (nm_lldp_listener_start(priv->lldp_listener, nm_device_get_ifindex(self), &error))
+                _LOGD(LOGD_DEVICE, "LLDP listener %p started", priv->lldp_listener);
+            else {
+                _LOGD(LOGD_DEVICE,
+                      "LLDP listener %p could not be started: %s",
+                      priv->lldp_listener,
+                      error->message);
+            }
+        }
+    } else {
+        if (priv->lldp_listener)
+            nm_lldp_listener_stop(priv->lldp_listener);
+    }
+}
+
+/* set-mode can be:
+ *  - TRUE: sync with new rules.
+ *  - FALSE: sync, but remove all rules (== flush)
+ *  - DEFAULT: forget about all the rules that we previously tracked,
+ *       but don't actually remove them. This is when quitting NM
+ *       we want to keep the rules.
+ *       The problem is, after restart of NM, the rule manager will
+ *       no longer remember that NM added these rules and treat them
+ *       as externally added ones. Don't restart NetworkManager if
+ *       you care about that.
+ */
+static void
+_routing_rules_sync(NMDevice *self, NMTernary set_mode)
+{
+    NMDevicePrivate *priv               = NM_DEVICE_GET_PRIVATE(self);
+    NMPRulesManager *rules_manager      = nm_netns_get_rules_manager(nm_device_get_netns(self));
+    NMDeviceClass *  klass              = NM_DEVICE_GET_CLASS(self);
+    gboolean         untrack_only_dirty = FALSE;
+    gboolean         keep_deleted_rules;
+    gpointer         user_tag_1;
+    gpointer         user_tag_2;
+
+    /* take two arbitrary user-tag pointers that belong to @self. */
+    user_tag_1 = &priv->v4_route_table;
+    user_tag_2 = &priv->v6_route_table;
+
+    if (set_mode == NM_TERNARY_TRUE) {
+        NMConnection *     applied_connection;
+        NMSettingIPConfig *s_ip;
+        guint              i, num;
+        int                is_ipv4;
+
+        untrack_only_dirty = TRUE;
+        nmp_rules_manager_set_dirty(rules_manager, user_tag_1);
+        if (klass->get_extra_rules)
+            nmp_rules_manager_set_dirty(rules_manager, user_tag_2);
+
+        applied_connection = nm_device_get_applied_connection(self);
+
+        for (is_ipv4 = 0; applied_connection && is_ipv4 < 2; is_ipv4++) {
+            int addr_family = is_ipv4 ? AF_INET : AF_INET6;
+
+            s_ip = nm_connection_get_setting_ip_config(applied_connection, addr_family);
+            if (!s_ip)
+                continue;
+
+            num = nm_setting_ip_config_get_num_routing_rules(s_ip);
+            for (i = 0; i < num; i++) {
+                NMPlatformRoutingRule plrule;
+                NMIPRoutingRule *     rule;
+
+                rule = nm_setting_ip_config_get_routing_rule(s_ip, i);
+                nm_ip_routing_rule_to_platform(rule, &plrule);
+
+                /* We track this rule, but we also make it explicitly not weakly-tracked
+                 * (meaning to untrack NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG at
+                 * the same time). */
+                nmp_rules_manager_track(rules_manager,
+                                        &plrule,
+                                        10,
+                                        user_tag_1,
+                                        NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+            }
+        }
+
+        if (klass->get_extra_rules) {
+            gs_unref_ptrarray GPtrArray *extra_rules = NULL;
+
+            extra_rules = klass->get_extra_rules(self);
+            if (extra_rules) {
+                for (i = 0; i < extra_rules->len; i++) {
+                    nmp_rules_manager_track(rules_manager,
+                                            NMP_OBJECT_CAST_ROUTING_RULE(extra_rules->pdata[i]),
+                                            10,
+                                            user_tag_2,
+                                            NMP_RULES_MANAGER_EXTERN_WEAKLY_TRACKED_USER_TAG);
+                }
+            }
+        }
+    }
+
+    nmp_rules_manager_untrack_all(rules_manager, user_tag_1, !untrack_only_dirty);
+    if (klass->get_extra_rules)
+        nmp_rules_manager_untrack_all(rules_manager, user_tag_2, !untrack_only_dirty);
+
+    keep_deleted_rules = FALSE;
+    if (set_mode == NM_TERNARY_DEFAULT) {
+        /* when exiting NM, we leave the device up and the rules configured.
+         * We just all nmp_rules_manager_sync() to forget about the synced rules,
+         * but we don't actually delete them.
+         *
+         * FIXME: that is a problem after restart of NetworkManager, because these
+         * rules will look like externally added, and NM will no longer remove
+         * them.
+         * To fix that, we could during "assume" mark the rules of the profile
+         * as owned (and "added" by the device). The problem with that is that it
+         * wouldn't cover rules that devices add by internal decision (not because
+         * of a setting in the profile, e.g. WireGuard could setup policy routing).
+         * Maybe it would be better to remember these orphaned rules at exit in a
+         * file and track them after restart again. */
+        keep_deleted_rules = TRUE;
+    }
+    nmp_rules_manager_sync(rules_manager, keep_deleted_rules);
+}
+
+static gboolean
+tc_commit(NMDevice *self)
+{
+    NMConnection *    connection          = NULL;
+    gs_unref_ptrarray GPtrArray *qdiscs   = NULL;
+    gs_unref_ptrarray GPtrArray *tfilters = NULL;
+    NMSettingTCConfig *          s_tc     = NULL;
+    NMPlatform *                 platform;
+    int                          ip_ifindex;
+
+    platform   = nm_device_get_platform(self);
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        s_tc = nm_connection_get_setting_tc_config(connection);
+
+    ip_ifindex = nm_device_get_ip_ifindex(self);
+    if (!ip_ifindex)
+        return s_tc == NULL;
+
+    if (s_tc) {
+        qdiscs   = nm_utils_qdiscs_from_tc_setting(platform, s_tc, ip_ifindex);
+        tfilters = nm_utils_tfilters_from_tc_setting(platform, s_tc, ip_ifindex);
+    }
+
+    if (!nm_platform_qdisc_sync(platform, ip_ifindex, qdiscs))
+        return FALSE;
+
+    if (!nm_platform_tfilter_sync(platform, ip_ifindex, tfilters))
+        return FALSE;
+
+    return TRUE;
+}
+
+/*
+ * activate_stage2_device_config
+ *
+ * Determine device parameters and set those on the device, ie
+ * for wireless devices, set SSID, keys, etc.
+ *
+ */
+static void
+activate_stage2_device_config(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceClass *  klass;
+    NMActStageReturn ret;
+    gboolean         no_firmware = FALSE;
+    CList *          iter;
+
+    nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+
+    if (!nm_device_sys_iface_state_is_external_or_assume(self))
+        _ethtool_state_set(self);
+
+    if (!nm_device_sys_iface_state_is_external_or_assume(self)) {
+        if (!tc_commit(self)) {
+            _LOGW(LOGD_IP6, "failed applying traffic control rules");
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            return;
+        }
+    }
+
+    _routing_rules_sync(self, NM_TERNARY_TRUE);
+
+    if (!nm_device_sys_iface_state_is_external_or_assume(self)) {
+        if (!nm_device_bring_up(self, FALSE, &no_firmware)) {
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    no_firmware ? NM_DEVICE_STATE_REASON_FIRMWARE_MISSING
+                                                : NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            return;
+        }
+    }
+
+    klass = NM_DEVICE_GET_CLASS(self);
+    if (klass->act_stage2_config_also_for_external_or_assume
+        || !nm_device_sys_iface_state_is_external_or_assume(self)) {
+        NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE;
+
+        ret = klass->act_stage2_config(self, &failure_reason);
+        if (ret == NM_ACT_STAGE_RETURN_POSTPONE)
+            return;
+        if (ret != NM_ACT_STAGE_RETURN_SUCCESS) {
+            nm_assert(ret == NM_ACT_STAGE_RETURN_FAILURE);
+            nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
+            return;
+        }
+    }
+
+    /* If we have slaves that aren't yet enslaved, do that now */
+    c_list_for_each (iter, &priv->slaves) {
+        SlaveInfo *   info        = c_list_entry(iter, SlaveInfo, lst_slave);
+        NMDeviceState slave_state = nm_device_get_state(info->slave);
+
+        if (slave_state == NM_DEVICE_STATE_IP_CONFIG)
+            nm_device_master_enslave_slave(self,
+                                           info->slave,
+                                           nm_device_get_applied_connection(info->slave));
+        else if (priv->act_request.obj && nm_device_sys_iface_state_is_external(self)
+                 && slave_state <= NM_DEVICE_STATE_DISCONNECTED)
+            nm_device_queue_recheck_assume(info->slave);
+    }
+
+    lldp_init(self, TRUE);
+
+    nm_device_activate_schedule_stage3_ip_config_start(self);
+}
+
+void
+nm_device_activate_schedule_stage2_device_config(NMDevice *self, gboolean do_sync)
+{
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    if (!do_sync) {
+        activation_source_schedule(self, activate_stage2_device_config, AF_INET);
+        return;
+    }
+
+    activation_source_invoke_sync(self, activate_stage2_device_config, AF_INET);
+}
+
+void
+nm_device_ip_method_failed(NMDevice *self, int addr_family, NMDeviceStateReason reason)
+{
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+
+    _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL);
+
+    if (get_ip_config_may_fail(self, addr_family))
+        check_ip_state(self, FALSE, (nm_device_get_state(self) == NM_DEVICE_STATE_IP_CONFIG));
+    else
+        nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, reason);
+}
+
+/*****************************************************************************/
+
+static void
+acd_data_destroy(gpointer ptr)
+{
+    AcdData *data = ptr;
+    int      i;
+
+    for (i = 0; data->configs && data->configs[i]; i++)
+        g_object_unref(data->configs[i]);
+    g_free(data->configs);
+    g_slice_free(AcdData, data);
+}
+
+static void
+ipv4_manual_method_apply(NMDevice *self, NMIP4Config **configs, gboolean success)
+{
+    NMConnection *connection;
+    const char *  method;
+
+    connection = nm_device_get_applied_connection(self);
+    nm_assert(connection);
+    method = nm_utils_get_ip_config_method(connection, AF_INET);
+    nm_assert(NM_IN_STRSET(method,
+                           NM_SETTING_IP4_CONFIG_METHOD_MANUAL,
+                           NM_SETTING_IP4_CONFIG_METHOD_AUTO));
+
+    if (!success) {
+        nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE);
+        return;
+    }
+
+    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_MANUAL))
+        nm_device_activate_schedule_ip_config_result(self, AF_INET, NULL);
+    else {
+        if (NM_DEVICE_GET_PRIVATE(self)->ip_state_4 != NM_DEVICE_IP_STATE_DONE)
+            ip_config_merge_and_apply(self, AF_INET, TRUE);
+    }
+}
+
+static void
+acd_manager_probe_terminated(NMAcdManager *acd_manager, gpointer user_data)
+{
+    AcdData *                   data = user_data;
+    NMDevice *                  self;
+    NMDevicePrivate *           priv;
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP4Address *address;
+    gboolean                    result, success = TRUE;
+    int                         i;
+
+    g_assert(data);
+    self = data->device;
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    for (i = 0; data->configs && data->configs[i]; i++) {
+        nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, data->configs[i], &address) {
+            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+            result = nm_acd_manager_check_address(acd_manager, address->address);
+            success &= result;
+
+            _NMLOG(result ? LOGL_DEBUG : LOGL_WARN,
+                   LOGD_DEVICE,
+                   "IPv4 DAD result: address %s is %s",
+                   _nm_utils_inet4_ntop(address->address, sbuf),
+                   result ? "unique" : "duplicate");
+        }
+    }
+
+    data->callback(self, data->configs, success);
+
+    priv->acd.dad_list = g_slist_remove(priv->acd.dad_list, acd_manager);
+    nm_acd_manager_free(acd_manager);
+}
+
+/**
+ * ipv4_dad_start:
+ * @self: device instance
+ * @configs: NULL-terminated array of IPv4 configurations
+ * @cb: callback function
+ *
+ * Start IPv4 DAD on device @self, check addresses in @configs and call @cb
+ * when the procedure ends. @cb will be called in any case, even if DAD can't
+ * be started. @configs will be unreferenced after @cb has been called.
+ */
+static void
+ipv4_dad_start(NMDevice *self, NMIP4Config **configs, AcdCallback cb)
+{
+    static const NMAcdCallbacks acd_callbacks = {
+        .probe_terminated_callback = acd_manager_probe_terminated,
+        .user_data_destroy         = acd_data_destroy,
+    };
+    NMDevicePrivate *           priv = NM_DEVICE_GET_PRIVATE(self);
+    NMAcdManager *              acd_manager;
+    const NMPlatformIP4Address *address;
+    NMDedupMultiIter            ipconf_iter;
+    AcdData *                   data;
+    guint                       timeout;
+    gboolean                    addr_found;
+    int                         r;
+    const guint8 *              hwaddr_arr;
+    size_t                      length;
+    guint                       i;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(configs);
+    g_return_if_fail(cb);
+
+    for (i = 0, addr_found = FALSE; configs[i]; i++) {
+        if (nm_ip4_config_get_num_addresses(configs[i]) > 0) {
+            addr_found = TRUE;
+            break;
+        }
+    }
+
+    timeout    = _prop_get_ipv4_dad_timeout(self);
+    hwaddr_arr = nm_platform_link_get_address(nm_device_get_platform(self),
+                                              nm_device_get_ip_ifindex(self),
+                                              &length);
+
+    if (!timeout || !hwaddr_arr || !addr_found || length != ETH_ALEN
+        || nm_device_sys_iface_state_is_external_or_assume(self)) {
+        /* DAD not needed, signal success */
+        cb(self, configs, TRUE);
+
+        for (i = 0; configs[i]; i++)
+            g_object_unref(configs[i]);
+        g_free(configs);
+
+        return;
+    }
+
+    data           = g_slice_new0(AcdData);
+    data->configs  = configs;
+    data->callback = cb;
+    data->device   = self;
+
+    acd_manager        = nm_acd_manager_new(nm_device_get_ip_ifindex(self),
+                                     hwaddr_arr,
+                                     length,
+                                     &acd_callbacks,
+                                     data);
+    priv->acd.dad_list = g_slist_append(priv->acd.dad_list, acd_manager);
+
+    for (i = 0; configs[i]; i++) {
+        nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, configs[i], &address)
+            nm_acd_manager_add_address(acd_manager, address->address);
+    }
+
+    r = nm_acd_manager_start_probe(acd_manager, timeout);
+    if (r < 0) {
+        _LOGW(LOGD_DEVICE, "acd probe failed");
+
+        /* DAD could not be started, signal success */
+        cb(self, configs, TRUE);
+
+        priv->acd.dad_list = g_slist_remove(priv->acd.dad_list, acd_manager);
+        nm_acd_manager_free(acd_manager);
+    }
+}
+
+/*****************************************************************************/
+/* IPv4LL stuff */
+
+static void
+ipv4ll_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->ipv4ll) {
+        sd_ipv4ll_set_callback(priv->ipv4ll, NULL, NULL);
+        sd_ipv4ll_stop(priv->ipv4ll);
+        priv->ipv4ll = sd_ipv4ll_unref(priv->ipv4ll);
+    }
+
+    nm_clear_g_source(&priv->ipv4ll_timeout);
+}
+
+static NMIP4Config *
+ipv4ll_get_ip4_config(NMDevice *self, guint32 lla)
+{
+    NMIP4Config *        config = NULL;
+    NMPlatformIP4Address address;
+    NMPlatformIP4Route   route;
+
+    config = nm_device_ip4_config_new(self);
+    g_assert(config);
+
+    memset(&address, 0, sizeof(address));
+    nm_platform_ip4_address_set_addr(&address, lla, 16);
+    address.addr_source = NM_IP_CONFIG_SOURCE_IP4LL;
+    nm_ip4_config_add_address(config, &address);
+
+    /* Add a multicast route for link-local connections: destination= 224.0.0.0, netmask=240.0.0.0 */
+    memset(&route, 0, sizeof(route));
+    route.network       = htonl(0xE0000000L);
+    route.plen          = 4;
+    route.rt_source     = NM_IP_CONFIG_SOURCE_IP4LL;
+    route.table_coerced = nm_platform_route_table_coerce(nm_device_get_route_table(self, AF_INET));
+    route.metric        = nm_device_get_route_metric(self, AF_INET);
+    nm_ip4_config_add_route(config, &route, NULL);
+
+    return config;
+}
+
+static void
+nm_device_handle_ipv4ll_event(sd_ipv4ll *ll, int event, void *data)
+{
+    NMDevice *       self = data;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    struct in_addr   address;
+    NMIP4Config *    config;
+    int              r;
+
+    if (priv->act_request.obj == NULL)
+        return;
+
+    nm_assert(nm_streq(nm_device_get_effective_ip_config_method(self, AF_INET),
+                       NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL));
+
+    switch (event) {
+    case SD_IPV4LL_EVENT_BIND:
+        r = sd_ipv4ll_get_address(ll, &address);
+        if (r < 0) {
+            _LOGE(LOGD_AUTOIP4, "invalid IPv4 link-local address received, error %d.", r);
+            nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED);
+            return;
+        }
+
+        if (!nm_utils_ip4_address_is_link_local(address.s_addr)) {
+            _LOGE(LOGD_AUTOIP4, "invalid address %08x received (not link-local).", address.s_addr);
+            nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_ERROR);
+            return;
+        }
+
+        config = ipv4ll_get_ip4_config(self, address.s_addr);
+        if (config == NULL) {
+            _LOGE(LOGD_AUTOIP4, "failed to get IPv4LL config");
+            nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED);
+            return;
+        }
+
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) {
+            nm_clear_g_source(&priv->ipv4ll_timeout);
+            nm_device_activate_schedule_ip_config_result(self, AF_INET, NM_IP_CONFIG_CAST(config));
+        } else if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) {
+            applied_config_init(&priv->dev_ip_config_4, config);
+            if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) {
+                _LOGE(LOGD_AUTOIP4, "failed to update IP4 config for autoip change.");
+                nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED);
+            }
+        } else
+            g_assert_not_reached();
+
+        g_object_unref(config);
+        break;
+    default:
+        _LOGW(LOGD_AUTOIP4, "IPv4LL address no longer valid after event %d.", event);
+        nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_AUTOIP_FAILED);
+    }
+}
+
+static gboolean
+ipv4ll_timeout_cb(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->ipv4ll_timeout) {
+        _LOGI(LOGD_AUTOIP4, "IPv4LL configuration timed out.");
+        priv->ipv4ll_timeout = 0;
+        ipv4ll_cleanup(self);
+
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF)
+            nm_device_activate_schedule_ip_config_timeout(self, AF_INET);
+    }
+
+    return FALSE;
+}
+
+static NMActStageReturn
+ipv4ll_start(NMDevice *self)
+{
+    NMDevicePrivate *        priv = NM_DEVICE_GET_PRIVATE(self);
+    const struct ether_addr *addr;
+    int                      ifindex, r;
+    size_t                   addr_len;
+
+    ipv4ll_cleanup(self);
+
+    r = sd_ipv4ll_new(&priv->ipv4ll);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: new() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    r = sd_ipv4ll_attach_event(priv->ipv4ll, NULL, 0);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: attach_event() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    addr    = nm_platform_link_get_address(nm_device_get_platform(self), ifindex, &addr_len);
+    if (!addr || addr_len != ETH_ALEN) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: can't retrieve hardware address");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    r = sd_ipv4ll_set_mac(priv->ipv4ll, addr);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: set_mac() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    r = sd_ipv4ll_set_ifindex(priv->ipv4ll, ifindex);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: set_ifindex() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    r = sd_ipv4ll_set_callback(priv->ipv4ll, nm_device_handle_ipv4ll_event, self);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: set_callback() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    r = sd_ipv4ll_start(priv->ipv4ll);
+    if (r < 0) {
+        _LOGE(LOGD_AUTOIP4, "IPv4LL: start() failed with error %d", r);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_AUTOIP4, "IPv4LL: started");
+
+    /* Start a timeout to bound the address attempt */
+    priv->ipv4ll_timeout = g_timeout_add_seconds(20, ipv4ll_timeout_cb, self);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static void
+ensure_con_ip_config(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMIPConfig *     con_ip_config;
+
+    if (priv->con_ip_config_x[IS_IPv4])
+        return;
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        return;
+
+    con_ip_config = nm_device_ip_config_new(self, addr_family);
+
+    if (IS_IPv4) {
+        nm_ip4_config_merge_setting(NM_IP4_CONFIG(con_ip_config),
+                                    nm_connection_get_setting_ip4_config(connection),
+                                    _prop_get_connection_mdns(self),
+                                    _prop_get_connection_llmnr(self),
+                                    nm_device_get_route_table(self, addr_family),
+                                    nm_device_get_route_metric(self, addr_family));
+    } else {
+        nm_ip6_config_merge_setting(NM_IP6_CONFIG(con_ip_config),
+                                    nm_connection_get_setting_ip6_config(connection),
+                                    nm_device_get_route_table(self, addr_family),
+                                    nm_device_get_route_metric(self, addr_family));
+    }
+
+    if (nm_device_sys_iface_state_is_external_or_assume(self)) {
+        /* For assumed connections ignore all addresses and routes. */
+        nm_ip_config_reset_addresses(con_ip_config);
+        nm_ip_config_reset_routes(con_ip_config);
+    }
+
+    priv->con_ip_config_x[IS_IPv4] = con_ip_config;
+}
+
+/*****************************************************************************/
+
+static void
+dhcp4_cleanup(NMDevice *self, CleanupType cleanup_type, gboolean release)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->dhcp_data_4.was_active = FALSE;
+    nm_clear_g_source(&priv->dhcp_data_4.grace_id);
+    priv->dhcp_data_4.grace_pending = FALSE;
+    nm_clear_g_free(&priv->dhcp4.pac_url);
+
+    if (priv->dhcp_data_4.client) {
+        /* Stop any ongoing DHCP transaction on this device */
+        nm_clear_g_signal_handler(priv->dhcp_data_4.client, &priv->dhcp_data_4.state_sigid);
+
+        if (cleanup_type == CLEANUP_TYPE_DECONFIGURE || cleanup_type == CLEANUP_TYPE_REMOVED)
+            nm_dhcp_client_stop(priv->dhcp_data_4.client, release);
+
+        g_clear_object(&priv->dhcp_data_4.client);
+    }
+
+    if (priv->dhcp_data_4.config) {
+        nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config);
+        _notify(self, PROP_DHCP4_CONFIG);
+    }
+}
+
+static gboolean
+ip_config_merge_and_apply(NMDevice *self, int addr_family, gboolean commit)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         success;
+    gs_unref_object NMIPConfig *composite = NULL;
+    NMIPConfig *                config;
+    gs_unref_ptrarray GPtrArray *ip4_dev_route_blacklist = NULL;
+    NMConnection *               connection;
+    gboolean                     ignore_auto_routes    = FALSE;
+    gboolean                     ignore_auto_dns       = FALSE;
+    gboolean                     ignore_default_routes = FALSE;
+    GSList *                     iter;
+    const char *                 ip6_addr_gen_token = NULL;
+    const int                    IS_IPv4            = NM_IS_IPv4(addr_family);
+
+    if (nm_device_sys_iface_state_is_external(self))
+        commit = FALSE;
+
+    connection = nm_device_get_applied_connection(self);
+
+    /* Apply ignore-auto-routes and ignore-auto-dns settings */
+    if (connection) {
+        NMSettingIPConfig *s_ip;
+
+        s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
+        if (s_ip) {
+            ignore_auto_routes = nm_setting_ip_config_get_ignore_auto_routes(s_ip);
+            ignore_auto_dns    = nm_setting_ip_config_get_ignore_auto_dns(s_ip);
+
+            /* if the connection has an explicit gateway, we also ignore
+             * the default routes from other sources. */
+            ignore_default_routes = nm_setting_ip_config_get_never_default(s_ip)
+                                    || nm_setting_ip_config_get_gateway(s_ip);
+
+            if (!IS_IPv4) {
+                NMSettingIP6Config *s_ip6 = NM_SETTING_IP6_CONFIG(s_ip);
+
+                if (nm_setting_ip6_config_get_addr_gen_mode(s_ip6)
+                    == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_EUI64)
+                    ip6_addr_gen_token = nm_setting_ip6_config_get_token(s_ip6);
+            }
+        }
+    }
+
+    composite = nm_device_ip_config_new(self, addr_family);
+
+    if (!IS_IPv4) {
+        nm_ip6_config_set_privacy(NM_IP6_CONFIG(composite),
+                                  priv->ndisc ? priv->ndisc_use_tempaddr
+                                              : NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+    }
+
+    init_ip_config_dns_priority(self, composite);
+
+    if (commit) {
+        if (priv->queued_ip_config_id_x[IS_IPv4])
+            update_ext_ip_config(self, addr_family, FALSE);
+        ensure_con_ip_config(self, addr_family);
+    }
+
+    if (!IS_IPv4) {
+        if (commit && priv->ipv6ll_has) {
+            const NMPlatformIP6Address ll_a = {
+                .address     = priv->ipv6ll_addr,
+                .plen        = 64,
+                .addr_source = NM_IP_CONFIG_SOURCE_IP6LL,
+            };
+            const NMPlatformIP6Route ll_r = {
+                .network.s6_addr16[0] = htons(0xfe80u),
+                .plen                 = 64,
+                .metric               = nm_device_get_route_metric(self, addr_family),
+                .rt_source            = NM_IP_CONFIG_SOURCE_IP6LL,
+            };
+
+            nm_assert(IN6_IS_ADDR_LINKLOCAL(&priv->ipv6ll_addr));
+
+            nm_ip6_config_add_address(NM_IP6_CONFIG(composite), &ll_a);
+            nm_ip6_config_add_route(NM_IP6_CONFIG(composite), &ll_r, NULL);
+        }
+    }
+
+    if (commit) {
+        gboolean v;
+
+        v = default_route_metric_penalty_detect(self, addr_family);
+        if (IS_IPv4)
+            priv->default_route_metric_penalty_ip4_has = v;
+        else
+            priv->default_route_metric_penalty_ip6_has = v;
+    }
+
+    /* Merge all the IP configs into the composite config */
+
+    if (IS_IPv4) {
+        config = applied_config_get_current(&priv->dev_ip_config_4);
+        if (config) {
+            nm_ip4_config_merge(
+                NM_IP4_CONFIG(composite),
+                NM_IP4_CONFIG(config),
+                (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0)
+                    | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0)
+                    | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0),
+                default_route_metric_penalty_get(self, addr_family));
+        }
+    }
+
+    if (!IS_IPv4) {
+        config = applied_config_get_current(&priv->ac_ip6_config);
+        if (config) {
+            nm_ip6_config_merge(
+                NM_IP6_CONFIG(composite),
+                NM_IP6_CONFIG(config),
+                (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0)
+                    | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0)
+                    | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0),
+                default_route_metric_penalty_get(self, addr_family));
+        }
+    }
+
+    if (!IS_IPv4) {
+        config = applied_config_get_current(&priv->dhcp6.ip6_config);
+        if (config) {
+            nm_ip6_config_merge(
+                NM_IP6_CONFIG(composite),
+                NM_IP6_CONFIG(config),
+                (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0)
+                    | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0)
+                    | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0),
+                default_route_metric_penalty_get(self, addr_family));
+        }
+    }
+
+    for (iter = priv->vpn_configs_x[IS_IPv4]; iter; iter = iter->next)
+        nm_ip_config_merge(composite, iter->data, NM_IP_CONFIG_MERGE_DEFAULT, 0);
+
+    if (priv->ext_ip_config_x[IS_IPv4])
+        nm_ip_config_merge(composite,
+                           priv->ext_ip_config_x[IS_IPv4],
+                           NM_IP_CONFIG_MERGE_EXTERNAL,
+                           0);
+
+    /* Merge WWAN config *last* to ensure modem-given settings overwrite
+     * any external stuff set by pppd or other scripts.
+     */
+    config = applied_config_get_current(&priv->dev2_ip_config_x[IS_IPv4]);
+    if (config) {
+        nm_ip_config_merge(composite,
+                           config,
+                           (ignore_auto_routes ? NM_IP_CONFIG_MERGE_NO_ROUTES : 0)
+                               | (ignore_default_routes ? NM_IP_CONFIG_MERGE_NO_DEFAULT_ROUTES : 0)
+                               | (ignore_auto_dns ? NM_IP_CONFIG_MERGE_NO_DNS : 0),
+                           default_route_metric_penalty_get(self, addr_family));
+    }
+
+    if (!IS_IPv4) {
+        if (priv->rt6_temporary_not_available) {
+            const NMPObject *o;
+            GHashTableIter   hiter;
+
+            g_hash_table_iter_init(&hiter, priv->rt6_temporary_not_available);
+            while (g_hash_table_iter_next(&hiter, (gpointer *) &o, NULL)) {
+                nm_ip6_config_add_route(NM_IP6_CONFIG(composite),
+                                        NMP_OBJECT_CAST_IP6_ROUTE(o),
+                                        NULL);
+            }
+        }
+    }
+
+    /* Merge user overrides into the composite config. For assumed connections,
+     * con_ip_config_x is empty. */
+    if (priv->con_ip_config_x[IS_IPv4]) {
+        nm_ip_config_merge(composite,
+                           priv->con_ip_config_x[IS_IPv4],
+                           NM_IP_CONFIG_MERGE_DEFAULT,
+                           default_route_metric_penalty_get(self, addr_family));
+    }
+
+    if (commit) {
+        gboolean is_vrf;
+
+        is_vrf = priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF;
+
+        if (IS_IPv4) {
+            nm_ip4_config_add_dependent_routes(NM_IP4_CONFIG(composite),
+                                               nm_device_get_route_table(self, addr_family),
+                                               nm_device_get_route_metric(self, addr_family),
+                                               is_vrf,
+                                               &ip4_dev_route_blacklist);
+        } else {
+            nm_ip6_config_add_dependent_routes(NM_IP6_CONFIG(composite),
+                                               nm_device_get_route_table(self, addr_family),
+                                               nm_device_get_route_metric(self, addr_family),
+                                               is_vrf);
+        }
+    }
+
+    if (IS_IPv4) {
+        if (commit) {
+            if (NM_DEVICE_GET_CLASS(self)->ip4_config_pre_commit)
+                NM_DEVICE_GET_CLASS(self)->ip4_config_pre_commit(self, NM_IP4_CONFIG(composite));
+        }
+    }
+
+    if (!IS_IPv4) {
+        NMUtilsIPv6IfaceId iid;
+
+        if (commit && priv->ndisc_started && ip6_addr_gen_token
+            && nm_utils_ipv6_interface_identifier_get_from_token(&iid, ip6_addr_gen_token)) {
+            set_ipv6_token(self, iid, ip6_addr_gen_token);
+        }
+    }
+
+    success =
+        nm_device_set_ip_config(self, addr_family, composite, commit, ip4_dev_route_blacklist);
+    if (commit) {
+        if (IS_IPv4)
+            priv->v4_commit_first_time = FALSE;
+        else
+            priv->v6_commit_first_time = FALSE;
+    }
+
+    return success;
+}
+
+static gboolean
+dhcp4_lease_change(NMDevice *self, NMIP4Config *config, gboolean bound)
+{
+    NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
+
+    g_return_val_if_fail(config, FALSE);
+
+    applied_config_init(&priv->dev_ip_config_4, config);
+
+    if (!ip_config_merge_and_apply(self, AF_INET, TRUE)) {
+        _LOGW(LOGD_DHCP4, "failed to update IPv4 config for DHCP change.");
+        return FALSE;
+    }
+
+    /* TODO: we should perform DAD again whenever we obtain a
+     * new lease after an expiry. But what should we do if
+     * a duplicate address is detected? Fail the connection;
+     * restart DHCP; continue without an address? */
+    if (bound && !nm_dhcp_client_accept(priv->dhcp_data_4.client, &error)) {
+        _LOGW(LOGD_DHCP4, "error accepting lease: %s", error->message);
+        return FALSE;
+    }
+
+    nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP4_CHANGE, self, NULL, NULL, NULL, NULL);
+
+    return TRUE;
+}
+
+static gboolean
+dhcp_grace_period_expired(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    priv->dhcp_data_x[IS_IPv4].grace_id      = 0;
+    priv->dhcp_data_x[IS_IPv4].grace_pending = FALSE;
+
+    _LOGI(LOGD_DHCPX(IS_IPv4),
+          "DHCPv%c: grace period expired",
+          nm_utils_addr_family_to_char(addr_family));
+
+    nm_device_ip_method_failed(self, addr_family, NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED);
+    /* If the device didn't fail, the DHCP client will continue */
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+dhcp_grace_period_expired_4(gpointer user_data)
+{
+    return dhcp_grace_period_expired(user_data, AF_INET);
+}
+
+static gboolean
+dhcp_grace_period_expired_6(gpointer user_data)
+{
+    return dhcp_grace_period_expired(user_data, AF_INET6);
+}
+
+static gboolean
+dhcp_grace_period_start(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    guint32          timeout;
+
+    /* In any other case (expired lease, assumed connection, etc.),
+     * wait for some time before failing the IP method.
+     */
+    if (priv->dhcp_data_x[IS_IPv4].grace_pending) {
+        /* already pending. */
+        return FALSE;
+    }
+
+    /* Start a grace period equal to the DHCP timeout multiplied
+     * by a constant factor. */
+    timeout = _prop_get_ipvx_dhcp_timeout(self, addr_family);
+    if (timeout == NM_DHCP_TIMEOUT_INFINITY)
+        _LOGI(LOGD_DHCPX(IS_IPv4),
+              "DHCPv%c: trying to acquire a new lease",
+              nm_utils_addr_family_to_char(addr_family));
+    else {
+        timeout = dhcp_grace_period_from_timeout(timeout);
+        _LOGI(LOGD_DHCPX(IS_IPv4),
+              "DHCPv%c: trying to acquire a new lease within %u seconds",
+              nm_utils_addr_family_to_char(addr_family),
+              timeout);
+        nm_assert(!priv->dhcp_data_x[IS_IPv4].grace_id);
+        priv->dhcp_data_x[IS_IPv4].grace_id = g_timeout_add_seconds(
+            timeout,
+            IS_IPv4 ? dhcp_grace_period_expired_4 : dhcp_grace_period_expired_6,
+            self);
+    }
+
+    priv->dhcp_data_x[IS_IPv4].grace_pending = TRUE;
+
+    return TRUE;
+}
+static void
+dhcp4_fail(NMDevice *self, NMDhcpState dhcp_state)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    _LOGD(LOGD_DHCP4,
+          "DHCPv4 failed (ip_state %s, was_active %d)",
+          _ip_state_to_string(priv->ip_state_4),
+          priv->dhcp_data_4.was_active);
+
+    /* The client is always left running after a failure. */
+
+    /* Nothing to do if we failed before... */
+    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL)
+        goto clear_config;
+
+    /* ... and also if there are static addresses configured
+     * on the interface.
+     */
+    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE && priv->con_ip_config_4
+        && nm_ip4_config_get_num_addresses(priv->con_ip_config_4) > 0)
+        goto clear_config;
+
+    /* Fail the method when one of the following is true:
+     * 1) the DHCP client terminated: it does not make sense to start a grace
+     *    period without a client running;
+     * 2) we failed to get an initial lease AND the client was
+     *    not active before.
+     */
+    if (dhcp_state == NM_DHCP_STATE_TERMINATED
+        || (!priv->dhcp_data_4.was_active && priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF)) {
+        nm_device_activate_schedule_ip_config_timeout(self, AF_INET);
+        return;
+    }
+
+    if (dhcp_grace_period_start(self, AF_INET))
+        goto clear_config;
+
+    return;
+
+clear_config:
+    /* The previous configuration is no longer valid */
+    if (priv->dhcp_data_4.config) {
+        nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config);
+        priv->dhcp_data_4.config = nm_dhcp_config_new(AF_INET);
+        _notify(self, PROP_DHCP4_CONFIG);
+    }
+}
+
+static void
+dhcp4_dad_cb(NMDevice *self, NMIP4Config **configs, gboolean success)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (success) {
+        nm_device_activate_schedule_ip_config_result(self, AF_INET, NM_IP_CONFIG_CAST(configs[1]));
+    } else {
+        nm_dhcp_client_decline(priv->dhcp_data_4.client, "Address conflict detected", NULL);
+        nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_IP_ADDRESS_DUPLICATE);
+    }
+}
+
+static void
+dhcp4_state_changed(NMDhcpClient *client,
+                    NMDhcpState   state,
+                    NMIP4Config * ip4_config,
+                    GHashTable *  options,
+                    gpointer      user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMIP4Config *    manual, **configs;
+    NMConnection *   connection;
+
+    g_return_if_fail(nm_dhcp_client_get_addr_family(client) == AF_INET);
+    g_return_if_fail(!ip4_config || NM_IS_IP4_CONFIG(ip4_config));
+
+    _LOGD(LOGD_DHCP4, "new DHCPv4 client state %d", state);
+
+    switch (state) {
+    case NM_DHCP_STATE_BOUND:
+    case NM_DHCP_STATE_EXTENDED:
+        if (!ip4_config) {
+            _LOGW(LOGD_DHCP4, "failed to get IPv4 config in response to DHCP event.");
+            dhcp4_fail(self, state);
+            break;
+        }
+
+        nm_clear_g_source(&priv->dhcp_data_4.grace_id);
+        priv->dhcp_data_4.grace_pending = FALSE;
+
+        /* After some failures, we have been able to renew the lease:
+         * update the ip state
+         */
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_FAIL)
+            _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_CONF);
+
+        g_free(priv->dhcp4.pac_url);
+        priv->dhcp4.pac_url = g_strdup(g_hash_table_lookup(options, "wpad"));
+        nm_device_set_proxy_config(self, priv->dhcp4.pac_url);
+
+        nm_dhcp_config_set_options(priv->dhcp_data_4.config, options);
+        _notify(self, PROP_DHCP4_CONFIG);
+
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF) {
+            connection = nm_device_get_applied_connection(self);
+            g_assert(connection);
+
+            manual = nm_device_ip4_config_new(self);
+            nm_ip4_config_merge_setting(manual,
+                                        nm_connection_get_setting_ip4_config(connection),
+                                        NM_SETTING_CONNECTION_MDNS_DEFAULT,
+                                        NM_SETTING_CONNECTION_LLMNR_DEFAULT,
+                                        nm_device_get_route_table(self, AF_INET),
+                                        nm_device_get_route_metric(self, AF_INET));
+
+            configs    = g_new0(NMIP4Config *, 3);
+            configs[0] = manual;
+            configs[1] = g_object_ref(ip4_config);
+
+            ipv4_dad_start(self, configs, dhcp4_dad_cb);
+        } else if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) {
+            if (dhcp4_lease_change(self, ip4_config, state == NM_DHCP_STATE_BOUND))
+                nm_device_update_metered(self);
+            else
+                dhcp4_fail(self, state);
+        }
+        break;
+    case NM_DHCP_STATE_TIMEOUT:
+        dhcp4_fail(self, state);
+        break;
+    case NM_DHCP_STATE_EXPIRE:
+        /* Ignore expiry before we even have a lease (NAK, old lease, etc) */
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_CONF)
+            break;
+        /* fall-through */
+    case NM_DHCP_STATE_DONE:
+    case NM_DHCP_STATE_FAIL:
+    case NM_DHCP_STATE_TERMINATED:
+        dhcp4_fail(self, state);
+        break;
+    default:
+        break;
+    }
+}
+
+static NMActStageReturn
+dhcp4_start(NMDevice *self)
+{
+    NMDevicePrivate *  priv = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingIPConfig *s_ip4;
+    gs_unref_bytes GBytes *vendor_class_identifier = NULL;
+    gs_unref_bytes GBytes *hwaddr                  = NULL;
+    gs_unref_bytes GBytes *bcast_hwaddr            = NULL;
+    gs_unref_bytes GBytes *client_id               = NULL;
+    gs_free char *         mud_url_free            = NULL;
+    NMConnection *         connection;
+    NMSettingConnection *  s_con;
+    GError *               error = NULL;
+    const NMPlatformLink * pllink;
+    const char *const *    reject_servers;
+
+    connection = nm_device_get_applied_connection(self);
+    g_return_val_if_fail(connection, FALSE);
+
+    s_ip4 = nm_connection_get_setting_ip4_config(connection);
+
+    s_con = nm_connection_get_setting_connection(connection);
+    nm_assert(s_con);
+
+    /* Clear old exported DHCP options */
+    nm_dbus_object_clear_and_unexport(&priv->dhcp_data_4.config);
+    priv->dhcp_data_4.config = nm_dhcp_config_new(AF_INET);
+
+    pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self));
+    if (pllink) {
+        hwaddr       = nmp_link_address_get_as_bytes(&pllink->l_address);
+        bcast_hwaddr = nmp_link_address_get_as_bytes(&pllink->l_broadcast);
+    }
+
+    client_id = _prop_get_ipv4_dhcp_client_id(self, connection, hwaddr);
+    vendor_class_identifier =
+        _prop_get_ipv4_dhcp_vendor_class_identifier(self, NM_SETTING_IP4_CONFIG(s_ip4));
+    reject_servers = nm_setting_ip_config_get_dhcp_reject_servers(s_ip4, NULL);
+
+    g_warn_if_fail(priv->dhcp_data_4.client == NULL);
+    priv->dhcp_data_4.client =
+        nm_dhcp_manager_start_ip4(nm_dhcp_manager_get(),
+                                  nm_netns_get_multi_idx(nm_device_get_netns(self)),
+                                  nm_device_get_ip_iface(self),
+                                  nm_device_get_ip_ifindex(self),
+                                  hwaddr,
+                                  bcast_hwaddr,
+                                  nm_connection_get_uuid(connection),
+                                  nm_device_get_route_table(self, AF_INET),
+                                  nm_device_get_route_metric(self, AF_INET),
+                                  nm_setting_ip_config_get_dhcp_send_hostname(s_ip4),
+                                  nm_setting_ip_config_get_dhcp_hostname(s_ip4),
+                                  nm_setting_ip4_config_get_dhcp_fqdn(NM_SETTING_IP4_CONFIG(s_ip4)),
+                                  _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET),
+                                  _prop_get_connection_mud_url(self, s_con, &mud_url_free),
+                                  client_id,
+                                  _prop_get_ipvx_dhcp_timeout(self, AF_INET),
+                                  priv->dhcp_anycast_address,
+                                  NULL,
+                                  vendor_class_identifier,
+                                  reject_servers,
+                                  &error);
+    if (!priv->dhcp_data_4.client) {
+        _LOGW(LOGD_DHCP4, "failure to start DHCP: %s", error->message);
+        g_clear_error(&error);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    priv->dhcp_data_4.state_sigid = g_signal_connect(priv->dhcp_data_4.client,
+                                                     NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED,
+                                                     G_CALLBACK(dhcp4_state_changed),
+                                                     self);
+
+    if (nm_device_sys_iface_state_is_external_or_assume(self))
+        priv->dhcp_data_4.was_active = TRUE;
+
+    /* DHCP devices will be notified by the DHCP manager when stuff happens */
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+gboolean
+nm_device_dhcp4_renew(NMDevice *self, gboolean release)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->dhcp_data_4.client != NULL, FALSE);
+
+    _LOGI(LOGD_DHCP4, "DHCPv4 lease renewal requested");
+
+    /* Terminate old DHCP instance and release the old lease */
+    dhcp4_cleanup(self, CLEANUP_TYPE_DECONFIGURE, release);
+
+    /* Start DHCP again on the interface */
+    return dhcp4_start(self) != NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+/*****************************************************************************/
+
+static NMIP4Config *
+shared4_new_config(NMDevice *self, NMConnection *connection)
+{
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMIP4Config *        config;
+    NMSettingIPConfig *  s_ip4;
+    NMPlatformIP4Address address = {
+        .addr_source = NM_IP_CONFIG_SOURCE_SHARED,
+    };
+
+    g_return_val_if_fail(self, NULL);
+    g_return_val_if_fail(connection, NULL);
+
+    s_ip4 = nm_connection_get_setting_ip4_config(connection);
+    if (s_ip4 && nm_setting_ip_config_get_num_addresses(s_ip4) > 0) {
+        /* Use the first user-supplied address */
+        NMIPAddress *user = nm_setting_ip_config_get_address(s_ip4, 0);
+        in_addr_t    a;
+
+        nm_ip_address_get_address_binary(user, &a);
+        nm_platform_ip4_address_set_addr(&address, a, nm_ip_address_get_prefix(user));
+        nm_clear_pointer(&priv->shared_ip_handle, nm_netns_shared_ip_release);
+    } else {
+        if (!priv->shared_ip_handle)
+            priv->shared_ip_handle = nm_netns_shared_ip_reserve(nm_device_get_netns(self));
+        nm_platform_ip4_address_set_addr(&address, priv->shared_ip_handle->addr, 24);
+    }
+
+    config = nm_device_ip4_config_new(self);
+    nm_ip4_config_add_address(config, &address);
+
+    return config;
+}
+
+/*****************************************************************************/
+
+static gboolean
+connection_ip_method_requires_carrier(NMConnection *connection,
+                                      int           addr_family,
+                                      gboolean *    out_ip_enabled)
+{
+    const char *method;
+
+    method = nm_utils_get_ip_config_method(connection, addr_family);
+
+    if (NM_IS_IPv4(addr_family)) {
+        NM_SET_OUT(out_ip_enabled, !nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED));
+        return NM_IN_STRSET(method,
+                            NM_SETTING_IP4_CONFIG_METHOD_AUTO,
+                            NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL);
+    }
+
+    NM_SET_OUT(out_ip_enabled,
+               !NM_IN_STRSET(method,
+                             NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                             NM_SETTING_IP6_CONFIG_METHOD_DISABLED));
+    return NM_IN_STRSET(method,
+                        NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                        NM_SETTING_IP6_CONFIG_METHOD_DHCP,
+                        NM_SETTING_IP6_CONFIG_METHOD_SHARED,
+                        NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL);
+}
+
+static gboolean
+connection_requires_carrier(NMConnection *connection)
+{
+    NMSettingIPConfig *  s_ip4, *s_ip6;
+    NMSettingConnection *s_con;
+    gboolean             ip4_carrier_wanted, ip6_carrier_wanted;
+    gboolean             ip4_used = FALSE, ip6_used = FALSE;
+
+    /* We can progress to IP_CONFIG now, so that we're enslaved.
+     * That may actually cause carrier to go up and thus continue activation. */
+    s_con = nm_connection_get_setting_connection(connection);
+    if (nm_setting_connection_get_master(s_con))
+        return FALSE;
+
+    ip4_carrier_wanted = connection_ip_method_requires_carrier(connection, AF_INET, &ip4_used);
+    if (ip4_carrier_wanted) {
+        /* If IPv4 wants a carrier and cannot fail, the whole connection
+         * requires a carrier regardless of the IPv6 method.
+         */
+        s_ip4 = nm_connection_get_setting_ip4_config(connection);
+        if (s_ip4 && !nm_setting_ip_config_get_may_fail(s_ip4))
+            return TRUE;
+    }
+
+    ip6_carrier_wanted = connection_ip_method_requires_carrier(connection, AF_INET6, &ip6_used);
+    if (ip6_carrier_wanted) {
+        /* If IPv6 wants a carrier and cannot fail, the whole connection
+         * requires a carrier regardless of the IPv4 method.
+         */
+        s_ip6 = nm_connection_get_setting_ip6_config(connection);
+        if (s_ip6 && !nm_setting_ip_config_get_may_fail(s_ip6))
+            return TRUE;
+    }
+
+    /* If an IP version wants a carrier and the other IP version isn't
+     * used, the connection requires carrier since it will just fail without one.
+     */
+    if (ip4_carrier_wanted && !ip6_used)
+        return TRUE;
+    if (ip6_carrier_wanted && !ip4_used)
+        return TRUE;
+
+    /* If both want a carrier, the whole connection wants a carrier */
+    return ip4_carrier_wanted && ip6_carrier_wanted;
+}
+
+static gboolean
+have_any_ready_slaves(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    SlaveInfo *      info;
+    CList *          iter;
+
+    /* Any enslaved slave is "ready" in the generic case as it's
+     * at least >= NM_DEVCIE_STATE_IP_CONFIG and has had Layer 2
+     * properties set up.
+     */
+    c_list_for_each (iter, &priv->slaves) {
+        info = c_list_entry(iter, SlaveInfo, lst_slave);
+        if (NM_DEVICE_GET_PRIVATE(info->slave)->is_enslaved)
+            return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+/* DHCPv6 stuff */
+
+static void
+dhcp6_cleanup(NMDevice *self, CleanupType cleanup_type, gboolean release)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->dhcp_data_6.was_active = FALSE;
+    priv->dhcp6.mode             = NM_NDISC_DHCP_LEVEL_NONE;
+    applied_config_clear(&priv->dhcp6.ip6_config);
+    nm_clear_g_free(&priv->dhcp6.event_id);
+    nm_clear_g_source(&priv->dhcp_data_6.grace_id);
+    priv->dhcp_data_6.grace_pending = FALSE;
+
+    if (priv->dhcp_data_6.client) {
+        nm_clear_g_signal_handler(priv->dhcp_data_6.client, &priv->dhcp_data_6.state_sigid);
+        nm_clear_g_signal_handler(priv->dhcp_data_6.client, &priv->dhcp6.prefix_sigid);
+
+        if (cleanup_type == CLEANUP_TYPE_DECONFIGURE || cleanup_type == CLEANUP_TYPE_REMOVED)
+            nm_dhcp_client_stop(priv->dhcp_data_6.client, release);
+
+        g_clear_object(&priv->dhcp_data_6.client);
+    }
+
+    if (priv->dhcp_data_6.config) {
+        nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config);
+        _notify(self, PROP_DHCP6_CONFIG);
+    }
+}
+
+static gboolean
+dhcp6_lease_change(NMDevice *self)
+{
+    NMDevicePrivate *     priv = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingsConnection *settings_connection;
+
+    if (!applied_config_get_current(&priv->dhcp6.ip6_config)) {
+        _LOGW(LOGD_DHCP6, "failed to get DHCPv6 config for rebind");
+        return FALSE;
+    }
+
+    g_assert(priv->dhcp_data_6.client); /* sanity check */
+
+    settings_connection = nm_device_get_settings_connection(self);
+    g_assert(settings_connection);
+
+    /* Apply the updated config */
+    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE)) {
+        _LOGW(LOGD_DHCP6, "failed to update IPv6 config in response to DHCP event");
+        return FALSE;
+    }
+
+    nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP6_CHANGE, self, NULL, NULL, NULL, NULL);
+
+    return TRUE;
+}
+
+static void
+dhcp6_fail(NMDevice *self, NMDhcpState dhcp_state)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         is_dhcp_managed;
+
+    _LOGD(LOGD_DHCP6,
+          "DHCPv6 failed (ip_state %s, was_active %d)",
+          _ip_state_to_string(priv->ip_state_6),
+          priv->dhcp_data_6.was_active);
+
+    /* The client is always left running after a failure. */
+
+    /* Nothing to do if we failed before... */
+    if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL)
+        goto clear_config;
+
+    is_dhcp_managed = (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED);
+
+    if (is_dhcp_managed) {
+        /* ... and also if there are static addresses configured
+         * on the interface.
+         */
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE && priv->con_ip_config_6
+            && nm_ip6_config_get_num_addresses(priv->con_ip_config_6))
+            goto clear_config;
+
+        /* Fail the method when one of the following is true:
+         * 1) the DHCP client terminated: it does not make sense to start a grace
+         *    period without a client running;
+         * 2) we failed to get an initial lease AND the client was
+         *    not active before.
+         */
+        if (dhcp_state == NM_DHCP_STATE_TERMINATED
+            || (!priv->dhcp_data_6.was_active && priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF)) {
+            nm_device_activate_schedule_ip_config_timeout(self, AF_INET6);
+            return;
+        }
+
+        if (dhcp_grace_period_start(self, AF_INET6))
+            goto clear_config;
+    } else {
+        /* not a hard failure; just live with the RA info */
+        dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, FALSE);
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF)
+            nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+    }
+    return;
+
+clear_config:
+    /* The previous configuration is no longer valid */
+    if (priv->dhcp_data_6.config) {
+        nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config);
+        priv->dhcp_data_6.config = nm_dhcp_config_new(AF_INET6);
+        _notify(self, PROP_DHCP6_CONFIG);
+    }
+}
+
+static void
+dhcp6_state_changed(NMDhcpClient *client,
+                    NMDhcpState   state,
+                    NMIP6Config * ip6_config,
+                    GHashTable *  options,
+                    gpointer      user_data)
+{
+    NMDevice *       self     = NM_DEVICE(user_data);
+    NMDevicePrivate *priv     = NM_DEVICE_GET_PRIVATE(self);
+    gs_free char *   event_id = NULL;
+
+    g_return_if_fail(nm_dhcp_client_get_addr_family(client) == AF_INET6);
+    g_return_if_fail(!ip6_config || NM_IS_IP6_CONFIG(ip6_config));
+
+    _LOGD(LOGD_DHCP6, "new DHCPv6 client state %d", state);
+
+    switch (state) {
+    case NM_DHCP_STATE_BOUND:
+    case NM_DHCP_STATE_EXTENDED:
+        nm_clear_g_source(&priv->dhcp_data_6.grace_id);
+        priv->dhcp_data_6.grace_pending = FALSE;
+        /* If the server sends multiple IPv6 addresses, we receive a state
+         * changed event for each of them. Use the event ID to merge IPv6
+         * addresses from the same transaction into a single configuration.
+         */
+
+        event_id = nm_dhcp_utils_get_dhcp6_event_id(options);
+
+        if (ip6_config && event_id && priv->dhcp6.event_id
+            && nm_streq(event_id, priv->dhcp6.event_id)) {
+            NMDedupMultiIter            ipconf_iter;
+            const NMPlatformIP6Address *a;
+
+            nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, ip6_config, &a)
+                applied_config_add_address(&priv->dhcp6.ip6_config, NM_PLATFORM_IP_ADDRESS_CAST(a));
+        } else {
+            nm_clear_g_free(&priv->dhcp6.event_id);
+            if (ip6_config) {
+                applied_config_init(&priv->dhcp6.ip6_config, ip6_config);
+                priv->dhcp6.event_id = g_strdup(event_id);
+                nm_dhcp_config_set_options(priv->dhcp_data_6.config, options);
+                _notify(self, PROP_DHCP6_CONFIG);
+            } else
+                applied_config_clear(&priv->dhcp6.ip6_config);
+        }
+
+        /* After long time we have been able to renew the lease:
+         * update the ip state
+         */
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL)
+            _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_CONF);
+
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) {
+            if (!applied_config_get_current(&priv->dhcp6.ip6_config)) {
+                nm_device_ip_method_failed(self, AF_INET6, NM_DEVICE_STATE_REASON_DHCP_FAILED);
+                break;
+            }
+            nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+        } else if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE)
+            if (!dhcp6_lease_change(self))
+                dhcp6_fail(self, state);
+        break;
+    case NM_DHCP_STATE_TIMEOUT:
+        if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED)
+            dhcp6_fail(self, state);
+        else {
+            /* not a hard failure; just live with the RA info */
+            dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, FALSE);
+            if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF)
+                nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+        }
+        break;
+    case NM_DHCP_STATE_EXPIRE:
+        /* Ignore expiry before we even have a lease (NAK, old lease, etc) */
+        if (priv->ip_state_6 != NM_DEVICE_IP_STATE_CONF)
+            dhcp6_fail(self, state);
+        break;
+    case NM_DHCP_STATE_TERMINATED:
+        /* In IPv6 info-only mode, the client doesn't handle leases so it
+         * may exit right after getting a response from the server.  That's
+         * normal.  In that case we just ignore the exit.
+         */
+        if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF)
+            break;
+        /* fall-through */
+    case NM_DHCP_STATE_DONE:
+    case NM_DHCP_STATE_FAIL:
+        dhcp6_fail(self, state);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+dhcp6_prefix_delegated(NMDhcpClient *client, NMPlatformIP6Address *prefix, gpointer user_data)
+{
+    NMDevice *self = NM_DEVICE(user_data);
+
+    /* Just re-emit. The device just contributes the prefix to the
+     * pool in NMPolicy, which decides about subnet allocation
+     * on the shared devices. */
+    g_signal_emit(self, signals[IP6_PREFIX_DELEGATED], 0, prefix);
+}
+
+/*****************************************************************************/
+
+static gboolean
+dhcp6_start_with_link_ready(NMDevice *self, NMConnection *connection)
+{
+    NMDevicePrivate *  priv = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingIPConfig *s_ip6;
+    gs_unref_bytes GBytes *hwaddr            = NULL;
+    gs_unref_bytes GBytes *     duid         = NULL;
+    gboolean                    enforce_duid = FALSE;
+    const NMPlatformLink *      pllink;
+    gs_free char *              mud_url_free = NULL;
+    GError *                    error        = NULL;
+    guint32                     iaid;
+    gboolean                    iaid_explicit;
+    NMSettingConnection *       s_con;
+    const NMPlatformIP6Address *ll_addr = NULL;
+
+    g_return_val_if_fail(connection, FALSE);
+
+    s_ip6 = nm_connection_get_setting_ip6_config(connection);
+    nm_assert(s_ip6);
+    s_con = nm_connection_get_setting_connection(connection);
+    nm_assert(s_con);
+
+    if (priv->ext_ip6_config_captured) {
+        ll_addr = nm_ip6_config_find_first_address(priv->ext_ip6_config_captured,
+                                                   NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL
+                                                       | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL);
+    }
+
+    if (!ll_addr) {
+        _LOGW(LOGD_DHCP6, "can't start DHCPv6: no link-local address");
+        return FALSE;
+    }
+
+    pllink = nm_platform_link_get(nm_device_get_platform(self), nm_device_get_ip_ifindex(self));
+    if (pllink)
+        hwaddr = nmp_link_address_get_as_bytes(&pllink->l_address);
+
+    iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET6, connection, TRUE, &iaid_explicit);
+    duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, &enforce_duid);
+
+    priv->dhcp_data_6.client = nm_dhcp_manager_start_ip6(
+        nm_dhcp_manager_get(),
+        nm_device_get_multi_index(self),
+        nm_device_get_ip_iface(self),
+        nm_device_get_ip_ifindex(self),
+        &ll_addr->address,
+        nm_connection_get_uuid(connection),
+        nm_device_get_route_table(self, AF_INET6),
+        nm_device_get_route_metric(self, AF_INET6),
+        nm_setting_ip_config_get_dhcp_send_hostname(s_ip6),
+        nm_setting_ip_config_get_dhcp_hostname(s_ip6),
+        _prop_get_ipvx_dhcp_hostname_flags(self, AF_INET6),
+        _prop_get_connection_mud_url(self, s_con, &mud_url_free),
+        duid,
+        enforce_duid,
+        iaid,
+        iaid_explicit,
+        _prop_get_ipvx_dhcp_timeout(self, AF_INET6),
+        priv->dhcp_anycast_address,
+        (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_OTHERCONF) ? TRUE : FALSE,
+        nm_setting_ip6_config_get_ip6_privacy(NM_SETTING_IP6_CONFIG(s_ip6)),
+        priv->dhcp6.needed_prefixes,
+        &error);
+    if (!priv->dhcp_data_6.client) {
+        _LOGW(LOGD_DHCP6, "failure to start DHCPv6: %s", error->message);
+        g_clear_error(&error);
+        if (nm_device_sys_iface_state_is_external_or_assume(self))
+            priv->dhcp_data_6.was_active = TRUE;
+        return FALSE;
+    }
+
+    priv->dhcp_data_6.state_sigid = g_signal_connect(priv->dhcp_data_6.client,
+                                                     NM_DHCP_CLIENT_SIGNAL_STATE_CHANGED,
+                                                     G_CALLBACK(dhcp6_state_changed),
+                                                     self);
+    priv->dhcp6.prefix_sigid      = g_signal_connect(priv->dhcp_data_6.client,
+                                                NM_DHCP_CLIENT_SIGNAL_PREFIX_DELEGATED,
+                                                G_CALLBACK(dhcp6_prefix_delegated),
+                                                self);
+
+    if (nm_device_sys_iface_state_is_external_or_assume(self))
+        priv->dhcp_data_6.was_active = TRUE;
+
+    return TRUE;
+}
+
+static gboolean
+dhcp6_start(NMDevice *self, gboolean wait_for_ll)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+
+    nm_dbus_object_clear_and_unexport(&priv->dhcp_data_6.config);
+    priv->dhcp_data_6.config = nm_dhcp_config_new(AF_INET6);
+
+    nm_assert(!applied_config_get_current(&priv->dhcp6.ip6_config));
+    applied_config_clear(&priv->dhcp6.ip6_config);
+    nm_clear_g_free(&priv->dhcp6.event_id);
+
+    connection = nm_device_get_applied_connection(self);
+    g_return_val_if_fail(connection, FALSE);
+
+    if (wait_for_ll) {
+        /* ensure link local is ready... */
+        if (!linklocal6_start(self)) {
+            /* wait for the LL address to show up */
+            return TRUE;
+        }
+        /* already have the LL address; kick off DHCP */
+    }
+
+    if (!dhcp6_start_with_link_ready(self, connection))
+        return FALSE;
+
+    return TRUE;
+}
+
+gboolean
+nm_device_dhcp6_renew(NMDevice *self, gboolean release)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMNDiscDHCPLevel mode;
+
+    g_return_val_if_fail(priv->dhcp_data_6.client != NULL, FALSE);
+
+    _LOGI(LOGD_DHCP6, "DHCPv6 lease renewal requested");
+
+    /* Terminate old DHCP instance and release the old lease */
+    mode = priv->dhcp6.mode;
+    dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, release);
+    priv->dhcp6.mode = mode;
+
+    /* Start DHCP again on the interface */
+    return dhcp6_start(self, FALSE);
+}
+
+/*****************************************************************************/
+
+/*
+ * Called on the requesting interface when a subnet can't be obtained
+ * from known prefixes for a newly active shared connection.
+ */
+void
+nm_device_request_ip6_prefixes(NMDevice *self, int needed_prefixes)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->dhcp6.needed_prefixes = needed_prefixes;
+
+    if (priv->dhcp_data_6.client) {
+        _LOGD(LOGD_IP6, "ipv6-pd: asking DHCPv6 for %d prefixes", needed_prefixes);
+        nm_device_dhcp6_renew(self, FALSE);
+    } else {
+        _LOGI(LOGD_IP6, "ipv6-pd: device doesn't use DHCPv6, can't request prefixes");
+    }
+}
+
+gboolean
+nm_device_needs_ip6_subnet(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->needs_ip6_subnet;
+}
+
+/*
+ * Called on the ipv6.method=shared interface when a new subnet is allocated
+ * or the prefix from which it is allocated is renewed.
+ */
+void
+nm_device_use_ip6_subnet(NMDevice *self, const NMPlatformIP6Address *subnet)
+{
+    NMDevicePrivate *    priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatformIP6Address address = *subnet;
+    char                 sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+    if (!applied_config_get_current(&priv->ac_ip6_config))
+        applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6);
+
+    /* Assign a ::1 address in the subnet for us. */
+    address.address.s6_addr32[3] |= htonl(1);
+    applied_config_add_address(&priv->ac_ip6_config, NM_PLATFORM_IP_ADDRESS_CAST(&address));
+
+    _LOGD(LOGD_IP6,
+          "ipv6-pd: using %s address (preferred for %u seconds)",
+          _nm_utils_inet6_ntop(&address.address, sbuf),
+          subnet->preferred);
+
+    /* This also updates the ndisc if there are actual changes. */
+    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+        _LOGW(LOGD_IP6, "ipv6-pd: failed applying IP6 config for connection sharing");
+}
+
+/*
+ * Called whenever the policy picks a default IPv6 device.
+ * The ipv6.method=shared devices just reuse its DNS configuration.
+ */
+void
+nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device)
+{
+    NMDevicePrivate *priv        = NM_DEVICE_GET_PRIVATE(self);
+    NMIP6Config *    from_config = NULL;
+    guint            i, len;
+
+    if (applied_config_get_current(&priv->ac_ip6_config)) {
+        applied_config_reset_nameservers(&priv->ac_ip6_config);
+        applied_config_reset_searches(&priv->ac_ip6_config);
+    } else
+        applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6);
+
+    if (from_device)
+        from_config = nm_device_get_ip6_config(from_device);
+    if (!from_config)
+        return;
+
+    len = nm_ip6_config_get_num_nameservers(from_config);
+    for (i = 0; i < len; i++) {
+        applied_config_add_nameserver(
+            &priv->ac_ip6_config,
+            (const NMIPAddr *) nm_ip6_config_get_nameserver(from_config, i));
+    }
+
+    len = nm_ip6_config_get_num_searches(from_config);
+    for (i = 0; i < len; i++) {
+        applied_config_add_search(&priv->ac_ip6_config, nm_ip6_config_get_search(from_config, i));
+    }
+
+    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+        _LOGW(LOGD_IP6, "ipv6-pd: failed applying DNS config for connection sharing");
+}
+
+/*****************************************************************************/
+
+static void
+linklocal6_failed(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->linklocal6_timeout_id);
+    nm_device_activate_schedule_ip_config_timeout(self, AF_INET6);
+}
+
+static gboolean
+linklocal6_timeout_cb(gpointer user_data)
+{
+    NMDevice *self = user_data;
+
+    _LOGD(LOGD_DEVICE, "linklocal6: waiting for link-local addresses failed due to timeout");
+    linklocal6_failed(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+linklocal6_check_complete(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+    const char *     method;
+
+    if (!priv->linklocal6_timeout_id) {
+        /* we are not waiting for linklocal to complete. Nothing to do. */
+        return;
+    }
+
+    if (!priv->ext_ip6_config_captured
+        || !nm_ip6_config_find_first_address(priv->ext_ip6_config_captured,
+                                             NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL
+                                                 | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL)) {
+        /* we don't have a non-tentative link local address yet. Wait longer. */
+        return;
+    }
+
+    nm_clear_g_source(&priv->linklocal6_timeout_id);
+
+    connection = nm_device_get_applied_connection(self);
+    g_assert(connection);
+
+    method = nm_device_get_effective_ip_config_method(self, AF_INET6);
+
+    _LOGD(LOGD_DEVICE,
+          "linklocal6: waiting for link-local addresses successful, continue with method %s",
+          method);
+
+    if (NM_IN_STRSET(method,
+                     NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                     NM_SETTING_IP6_CONFIG_METHOD_SHARED))
+        addrconf6_start_with_link_ready(self);
+    else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DHCP)) {
+        if (!dhcp6_start_with_link_ready(self, connection)) {
+            /* Time out IPv6 instead of failing the entire activation */
+            nm_device_activate_schedule_ip_config_timeout(self, AF_INET6);
+        }
+    } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL))
+        nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+    else
+        g_return_if_fail(FALSE);
+}
+
+static void
+check_and_add_ipv6ll_addr(NMDevice *self)
+{
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    struct in6_addr     lladdr;
+    NMConnection *      connection;
+    NMSettingIP6Config *s_ip6 = NULL;
+    GError *            error = NULL;
+    const char *        addr_type;
+    char                sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+    if (!priv->ipv6ll_handle)
+        return;
+
+    if (priv->ext_ip6_config_captured
+        && nm_ip6_config_find_first_address(priv->ext_ip6_config_captured,
+                                            NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL
+                                                | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL
+                                                | NM_PLATFORM_MATCH_WITH_ADDRSTATE_TENTATIVE)) {
+        /* Already have an LL address, nothing to do */
+        return;
+    }
+
+    priv->ipv6ll_has = FALSE;
+    memset(&priv->ipv6ll_addr, 0, sizeof(priv->ipv6ll_addr));
+
+    memset(&lladdr, 0, sizeof(lladdr));
+    lladdr.s6_addr16[0] = htons(0xfe80);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        s_ip6 = NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection));
+
+    if (s_ip6
+        && nm_setting_ip6_config_get_addr_gen_mode(s_ip6)
+               == NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE_STABLE_PRIVACY) {
+        NMUtilsStableType stable_type;
+        const char *      stable_id;
+
+        stable_id = _prop_get_connection_stable_id(self, connection, &stable_type);
+        if (!nm_utils_ipv6_addr_set_stable_privacy(stable_type,
+                                                   &lladdr,
+                                                   nm_device_get_iface(self),
+                                                   stable_id,
+                                                   priv->linklocal6_dad_counter++,
+                                                   &error)) {
+            _LOGW(LOGD_IP6, "linklocal6: failed to generate an address: %s", error->message);
+            g_clear_error(&error);
+            linklocal6_failed(self);
+            return;
+        }
+        addr_type = "stable-privacy";
+    } else {
+        NMUtilsIPv6IfaceId iid;
+
+        if (priv->linklocal6_timeout_id) {
+            /* We already started and attempt to add a LL address. For the EUI-64
+             * mode we can't pick a new one, we'll just fail. */
+            _LOGW(LOGD_IP6, "linklocal6: DAD failed for an EUI-64 address");
+            linklocal6_failed(self);
+            return;
+        }
+
+        if (!nm_device_get_ip_iface_identifier(self, &iid, TRUE)) {
+            _LOGW(LOGD_IP6, "linklocal6: failed to get interface identifier; IPv6 cannot continue");
+            return;
+        }
+        nm_utils_ipv6_addr_set_interface_identifier(&lladdr, iid);
+        addr_type = "EUI-64";
+    }
+
+    _LOGD(LOGD_IP6,
+          "linklocal6: generated %s IPv6LL address %s",
+          addr_type,
+          _nm_utils_inet6_ntop(&lladdr, sbuf));
+    priv->ipv6ll_has  = TRUE;
+    priv->ipv6ll_addr = lladdr;
+    ip_config_merge_and_apply(self, AF_INET6, TRUE);
+}
+
+static gboolean
+linklocal6_start(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->linklocal6_timeout_id);
+
+    if (priv->ext_ip6_config_captured
+        && nm_ip6_config_find_first_address(priv->ext_ip6_config_captured,
+                                            NM_PLATFORM_MATCH_WITH_ADDRTYPE_LINKLOCAL
+                                                | NM_PLATFORM_MATCH_WITH_ADDRSTATE_NORMAL))
+        return TRUE;
+
+    _LOGD(LOGD_DEVICE,
+          "linklocal6: starting IPv6 with method '%s', but the device has no link-local addresses "
+          "configured. Wait.",
+          nm_device_get_effective_ip_config_method(self, AF_INET6));
+
+    check_and_add_ipv6ll_addr(self);
+
+    /* Depending on the network and what the 'dad_transmits' and 'retrans_time_ms'
+     * sysctl values are, DAD for the IPv6LL address may take quite a while.
+     * FIXME: use dad/retrans sysctl values if they are higher than a minimum time.
+     * (rh #1101809)
+     */
+    priv->linklocal6_timeout_id = g_timeout_add_seconds(15, linklocal6_timeout_cb, self);
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+gint64
+nm_device_get_configured_mtu_from_connection_default(NMDevice *  self,
+                                                     const char *property_name,
+                                                     guint32     max_mtu)
+{
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       property_name,
+                                                       self,
+                                                       0,
+                                                       max_mtu,
+                                                       -1);
+}
+
+guint32
+nm_device_get_configured_mtu_from_connection(NMDevice *         self,
+                                             GType              setting_type,
+                                             NMDeviceMtuSource *out_source)
+{
+    const char *  global_property_name;
+    NMConnection *connection;
+    NMSetting *   setting;
+    gint64        mtu_default;
+    guint32       mtu     = 0;
+    guint32       max_mtu = G_MAXUINT32;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(out_source);
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        g_return_val_if_reached(0);
+
+    setting = nm_connection_get_setting(connection, setting_type);
+
+    if (setting_type == NM_TYPE_SETTING_WIRED) {
+        if (setting)
+            mtu = nm_setting_wired_get_mtu(NM_SETTING_WIRED(setting));
+        global_property_name = NM_CON_DEFAULT("ethernet.mtu");
+    } else if (setting_type == NM_TYPE_SETTING_WIRELESS) {
+        if (setting)
+            mtu = nm_setting_wireless_get_mtu(NM_SETTING_WIRELESS(setting));
+        global_property_name = NM_CON_DEFAULT("wifi.mtu");
+    } else if (setting_type == NM_TYPE_SETTING_INFINIBAND) {
+        if (setting)
+            mtu = nm_setting_infiniband_get_mtu(NM_SETTING_INFINIBAND(setting));
+        global_property_name = NM_CON_DEFAULT("infiniband.mtu");
+        max_mtu              = NM_INFINIBAND_MAX_MTU;
+    } else if (setting_type == NM_TYPE_SETTING_IP_TUNNEL) {
+        if (setting)
+            mtu = nm_setting_ip_tunnel_get_mtu(NM_SETTING_IP_TUNNEL(setting));
+        global_property_name = NM_CON_DEFAULT("ip-tunnel.mtu");
+    } else if (setting_type == NM_TYPE_SETTING_WIREGUARD) {
+        if (setting)
+            mtu = nm_setting_wireguard_get_mtu(NM_SETTING_WIREGUARD(setting));
+        global_property_name = NM_CON_DEFAULT("wireguard.mtu");
+    } else
+        g_return_val_if_reached(0);
+
+    if (mtu) {
+        *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+        return mtu;
+    }
+
+    mtu_default =
+        nm_device_get_configured_mtu_from_connection_default(self, global_property_name, max_mtu);
+    if (mtu_default >= 0) {
+        *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+        return (guint32) mtu_default;
+    }
+
+    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
+    return 0;
+}
+
+guint32
+nm_device_get_configured_mtu_for_wired(NMDevice *         self,
+                                       NMDeviceMtuSource *out_source,
+                                       gboolean *         out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(self, NM_TYPE_SETTING_WIRED, out_source);
+}
+
+guint32
+nm_device_get_configured_mtu_wired_parent(NMDevice *         self,
+                                          NMDeviceMtuSource *out_source,
+                                          gboolean *         out_force)
+{
+    guint32 mtu        = 0;
+    guint32 parent_mtu = 0;
+    int     ifindex;
+
+    ifindex = nm_device_parent_get_ifindex(self);
+    if (ifindex > 0) {
+        parent_mtu = nm_platform_link_get_mtu(nm_device_get_platform(self), ifindex);
+        if (parent_mtu >= NM_DEVICE_GET_CLASS(self)->mtu_parent_delta)
+            parent_mtu -= NM_DEVICE_GET_CLASS(self)->mtu_parent_delta;
+        else
+            parent_mtu = 0;
+    }
+
+    mtu = nm_device_get_configured_mtu_for_wired(self, out_source, NULL);
+
+    if (parent_mtu && mtu > parent_mtu) {
+        /* Trying to set a MTU that is out of range from configuration:
+         * fall back to the parent MTU and set force flag so that it
+         * overrides an MTU with higher priority already configured.
+         */
+        *out_source = NM_DEVICE_MTU_SOURCE_PARENT;
+        *out_force  = TRUE;
+        return parent_mtu;
+    }
+
+    if (*out_source != NM_DEVICE_MTU_SOURCE_NONE) {
+        nm_assert(mtu > 0);
+        return mtu;
+    }
+
+    /* Inherit the MTU from parent device, if any */
+    if (parent_mtu) {
+        mtu         = parent_mtu;
+        *out_source = NM_DEVICE_MTU_SOURCE_PARENT;
+    }
+
+    return mtu;
+}
+
+/*****************************************************************************/
+
+static void
+_set_mtu(NMDevice *self, guint32 mtu)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->mtu == mtu)
+        return;
+
+    priv->mtu = mtu;
+    _notify(self, PROP_MTU);
+
+    if (priv->master) {
+        /* changing the MTU of a slave, might require the master to reset
+         * its MTU. Note that the master usually cannot set a MTU larger
+         * then the slave's. Hence, when the slave increases the MTU,
+         * master might want to retry setting the MTU. */
+        nm_device_commit_mtu(priv->master);
+    }
+}
+
+static gboolean
+set_platform_mtu(NMDevice *self, guint32 mtu)
+{
+    int r;
+
+    r = nm_platform_link_set_mtu(nm_device_get_platform(self), nm_device_get_ip_ifindex(self), mtu);
+    return (r != -NME_PL_CANT_SET_MTU);
+}
+
+static void
+_commit_mtu(NMDevice *self, const NMIP4Config *config)
+{
+    NMDevicePrivate * priv   = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceMtuSource source = NM_DEVICE_MTU_SOURCE_NONE;
+    guint32           ip6_mtu, ip6_mtu_orig;
+    guint32           mtu_desired, mtu_desired_orig;
+    guint32           mtu_plat;
+    struct {
+        gboolean initialized;
+        guint32  value;
+    } ip6_mtu_sysctl = {
+        0,
+    };
+    int      ifindex;
+    char     sbuf[64], sbuf1[64], sbuf2[64];
+    gboolean success = TRUE;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        return;
+
+    if (!nm_device_get_applied_connection(self)
+        || nm_device_sys_iface_state_is_external_or_assume(self)) {
+        /* we don't tamper with the MTU of disconnected and
+         * external/assumed devices. */
+        return;
+    }
+
+    {
+        guint32  mtu   = 0;
+        gboolean force = FALSE;
+
+        /* We take the MTU from various sources: (in order of increasing
+         * priority) parent link, IP configuration (which contains the
+         * MTU from DHCP/PPP), connection profile.
+         *
+         * We could just compare it with the platform MTU and apply it
+         * when different, but this would revert at random times manual
+         * changes done by the user with the MTU from the connection.
+         *
+         * Instead, we remember the source of the currently configured
+         * MTU and apply the new one only when the new source has a
+         * higher priority, so that we don't set a MTU from same source
+         * multiple times. An exception to this is for the PARENT
+         * source, since we need to keep tracking the parent MTU when it
+         * changes.
+         *
+         * The subclass can set the @force argument to TRUE to signal that the
+         * returned MTU should be applied even if it has a lower priority. This
+         * is useful when the value from a lower source should
+         * preempt the one from higher ones.
+         */
+
+        if (NM_DEVICE_GET_CLASS(self)->get_configured_mtu)
+            mtu = NM_DEVICE_GET_CLASS(self)->get_configured_mtu(self, &source, &force);
+
+        if (config && !force && source < NM_DEVICE_MTU_SOURCE_IP_CONFIG
+            && nm_ip4_config_get_mtu(config)) {
+            mtu    = nm_ip4_config_get_mtu(config);
+            source = NM_DEVICE_MTU_SOURCE_IP_CONFIG;
+        }
+
+        if (mtu != 0) {
+            _LOGT(LOGD_DEVICE,
+                  "mtu: value %u from source '%s' (%u), current source '%s' (%u)%s",
+                  (guint) mtu,
+                  mtu_source_to_str(source),
+                  (guint) source,
+                  mtu_source_to_str(priv->mtu_source),
+                  (guint) priv->mtu_source,
+                  force ? " (forced)" : "");
+        }
+
+        if (mtu != 0
+            && (force || source > priv->mtu_source
+                || (priv->mtu_source == NM_DEVICE_MTU_SOURCE_PARENT && source == priv->mtu_source)))
+            mtu_desired = mtu;
+        else {
+            mtu_desired = 0;
+            source      = NM_DEVICE_MTU_SOURCE_NONE;
+        }
+    }
+
+    if (mtu_desired && mtu_desired < 1280) {
+        NMSettingIPConfig *s_ip6;
+
+        s_ip6 = nm_device_get_applied_setting(self, NM_TYPE_SETTING_IP6_CONFIG);
+        if (s_ip6
+            && !NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip6),
+                             NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                             NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) {
+            /* the interface has IPv6 enabled. The MTU with IPv6 cannot be smaller
+             * then 1280.
+             *
+             * For slave-devices (that don't have @s_ip6 we) don't do this fixup because
+             * it's anyway an unsolved problem when the slave configures a conflicting
+             * MTU. */
+            mtu_desired = 1280;
+        }
+    }
+
+    ip6_mtu = priv->ip6_mtu;
+    if (!ip6_mtu && priv->mtu_source == NM_DEVICE_MTU_SOURCE_NONE) {
+        /* initially, if the IPv6 MTU is not specified, grow it as large as the
+         * link MTU @mtu_desired. Only exception is, if @mtu_desired is so small
+         * to disable IPv6. */
+        if (mtu_desired >= 1280)
+            ip6_mtu = mtu_desired;
+    }
+
+    if (!ip6_mtu && !mtu_desired)
+        return;
+
+    mtu_desired_orig = mtu_desired;
+    ip6_mtu_orig     = ip6_mtu;
+
+    mtu_plat = nm_platform_link_get_mtu(nm_device_get_platform(self), ifindex);
+
+    if (ip6_mtu) {
+        ip6_mtu = NM_MAX(1280, ip6_mtu);
+
+        if (!mtu_desired)
+            mtu_desired = mtu_plat;
+
+        if (mtu_desired) {
+            mtu_desired = NM_MAX(1280, mtu_desired);
+
+            if (mtu_desired < ip6_mtu)
+                ip6_mtu = mtu_desired;
+        }
+    }
+
+    if (mtu_desired && NM_DEVICE_GET_CLASS(self)->mtu_force_set && !priv->mtu_force_set_done) {
+        priv->mtu_force_set_done = TRUE;
+
+        if (mtu_desired == mtu_plat) {
+            mtu_plat--;
+            if (NM_DEVICE_GET_CLASS(self)->set_platform_mtu(self, mtu_desired - 1)) {
+                _LOGD(LOGD_DEVICE, "mtu: force-set MTU to %u", mtu_desired - 1);
+            } else
+                _LOGW(LOGD_DEVICE, "mtu: failure to force-set MTU to %u", mtu_desired - 1);
+        }
+    }
+
+    _LOGT(LOGD_DEVICE,
+          "mtu: device-mtu: %u%s, ipv6-mtu: %u%s, ifindex: %d",
+          (guint) mtu_desired,
+          mtu_desired == mtu_desired_orig
+              ? ""
+              : nm_sprintf_buf(sbuf1, " (was %u)", (guint) mtu_desired_orig),
+          (guint) ip6_mtu,
+          ip6_mtu == ip6_mtu_orig ? "" : nm_sprintf_buf(sbuf2, " (was %u)", (guint) ip6_mtu_orig),
+          ifindex);
+
+#define _IP6_MTU_SYS()                                                                         \
+    ({                                                                                         \
+        if (!ip6_mtu_sysctl.initialized) {                                                     \
+            ip6_mtu_sysctl.value       = nm_device_sysctl_ip_conf_get_int_checked(self,        \
+                                                                            AF_INET6,    \
+                                                                            "mtu",       \
+                                                                            10,          \
+                                                                            0,           \
+                                                                            G_MAXUINT32, \
+                                                                            0);          \
+            ip6_mtu_sysctl.initialized = TRUE;                                                 \
+        }                                                                                      \
+        ip6_mtu_sysctl.value;                                                                  \
+    })
+    if ((mtu_desired && mtu_desired != mtu_plat) || (ip6_mtu && ip6_mtu != _IP6_MTU_SYS())) {
+        gboolean anticipated_failure = FALSE;
+
+        if (!priv->mtu_initial && !priv->ip6_mtu_initial) {
+            /* before touching any of the MTU parameters, record the
+             * original setting to restore on deactivation. */
+            priv->mtu_initial     = mtu_plat;
+            priv->ip6_mtu_initial = _IP6_MTU_SYS();
+        }
+
+        if (mtu_desired && mtu_desired != mtu_plat) {
+            if (!NM_DEVICE_GET_CLASS(self)->set_platform_mtu(self, mtu_desired)) {
+                anticipated_failure = TRUE;
+                success             = FALSE;
+                _LOGW(LOGD_DEVICE,
+                      "mtu: failure to set MTU. %s",
+                      NM_IS_DEVICE_VLAN(self)
+                          ? "Is the parent's MTU size large enough?"
+                          : (!c_list_is_empty(&priv->slaves)
+                                 ? "Are the MTU sizes of the slaves large enough?"
+                                 : "Did you configure the MTU correctly?"));
+            }
+            priv->carrier_wait_until_ms =
+                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+        }
+
+        if (ip6_mtu && ip6_mtu != _IP6_MTU_SYS()) {
+            if (!nm_device_sysctl_ip_conf_set(self,
+                                              AF_INET6,
+                                              "mtu",
+                                              nm_sprintf_buf(sbuf, "%u", (unsigned) ip6_mtu))) {
+                int         errsv = errno;
+                NMLogLevel  level = LOGL_WARN;
+                const char *msg   = NULL;
+
+                success = FALSE;
+
+                if (anticipated_failure && errsv == EINVAL) {
+                    level = LOGL_DEBUG;
+                    msg   = "Is the underlying MTU value successfully set?";
+                } else if (!g_file_test("/proc/sys/net/ipv6", G_FILE_TEST_IS_DIR)) {
+                    level   = LOGL_DEBUG;
+                    msg     = "IPv6 is disabled";
+                    success = TRUE;
+                }
+
+                _NMLOG(level,
+                       LOGD_DEVICE,
+                       "mtu: failure to set IPv6 MTU%s%s",
+                       msg ? ": " : "",
+                       msg ?: "");
+            }
+            priv->carrier_wait_until_ms =
+                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+        }
+    }
+
+    if (success && source != NM_DEVICE_MTU_SOURCE_NONE)
+        priv->mtu_source = source;
+
+#undef _IP6_MTU_SYS
+}
+
+void
+nm_device_commit_mtu(NMDevice *self)
+{
+    NMDeviceState state;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    state = nm_device_get_state(self);
+    if (state >= NM_DEVICE_STATE_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
+        _LOGT(LOGD_DEVICE, "mtu: commit-mtu...");
+        _commit_mtu(self, NM_DEVICE_GET_PRIVATE(self)->ip_config_4);
+    } else
+        _LOGT(LOGD_DEVICE,
+              "mtu: commit-mtu... skip due to state %s",
+              nm_device_state_to_str(state));
+}
+
+static void
+ndisc_config_changed(NMNDisc *ndisc, const NMNDiscData *rdata, guint changed_int, NMDevice *self)
+{
+    NMNDiscConfigMap changed = changed_int;
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    guint            i;
+
+    g_return_if_fail(priv->act_request.obj);
+
+    if (!applied_config_get_current(&priv->ac_ip6_config))
+        applied_config_init_new(&priv->ac_ip6_config, self, AF_INET6);
+
+    if (changed & NM_NDISC_CONFIG_ADDRESSES) {
+        guint8  plen;
+        guint32 ifa_flags;
+
+        /* Check, whether kernel is recent enough to help user space handling RA.
+         * If it's not supported, we have no ipv6-privacy and must add autoconf
+         * addresses as /128. The reason for the /128 is to prevent the kernel
+         * from adding a prefix route for this address. */
+        ifa_flags = 0;
+        if (nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)) {
+            ifa_flags |= IFA_F_NOPREFIXROUTE;
+            if (NM_IN_SET(priv->ndisc_use_tempaddr,
+                          NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR,
+                          NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR))
+                ifa_flags |= IFA_F_MANAGETEMPADDR;
+            plen = 64;
+        } else
+            plen = 128;
+
+        nm_ip6_config_reset_addresses_ndisc((NMIP6Config *) priv->ac_ip6_config.orig,
+                                            rdata->addresses,
+                                            rdata->addresses_n,
+                                            plen,
+                                            ifa_flags);
+        if (priv->ac_ip6_config.current) {
+            nm_ip6_config_reset_addresses_ndisc((NMIP6Config *) priv->ac_ip6_config.current,
+                                                rdata->addresses,
+                                                rdata->addresses_n,
+                                                plen,
+                                                ifa_flags);
+        }
+    }
+
+    if (NM_FLAGS_ANY(changed, NM_NDISC_CONFIG_ROUTES | NM_NDISC_CONFIG_GATEWAYS)) {
+        nm_ip6_config_reset_routes_ndisc(
+            (NMIP6Config *) priv->ac_ip6_config.orig,
+            rdata->gateways,
+            rdata->gateways_n,
+            rdata->routes,
+            rdata->routes_n,
+            nm_device_get_route_table(self, AF_INET6),
+            nm_device_get_route_metric(self, AF_INET6),
+            nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF));
+        if (priv->ac_ip6_config.current) {
+            nm_ip6_config_reset_routes_ndisc(
+                (NMIP6Config *) priv->ac_ip6_config.current,
+                rdata->gateways,
+                rdata->gateways_n,
+                rdata->routes,
+                rdata->routes_n,
+                nm_device_get_route_table(self, AF_INET6),
+                nm_device_get_route_metric(self, AF_INET6),
+                nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_RTA_PREF));
+        }
+    }
+
+    if (changed & NM_NDISC_CONFIG_DNS_SERVERS) {
+        /* Rebuild DNS server list from neighbor discovery cache. */
+        applied_config_reset_nameservers(&priv->ac_ip6_config);
+
+        for (i = 0; i < rdata->dns_servers_n; i++)
+            applied_config_add_nameserver(&priv->ac_ip6_config,
+                                          (const NMIPAddr *) &rdata->dns_servers[i].address);
+    }
+
+    if (changed & NM_NDISC_CONFIG_DNS_DOMAINS) {
+        /* Rebuild domain list from neighbor discovery cache. */
+        applied_config_reset_searches(&priv->ac_ip6_config);
+
+        for (i = 0; i < rdata->dns_domains_n; i++)
+            applied_config_add_search(&priv->ac_ip6_config, rdata->dns_domains[i].domain);
+    }
+
+    if (changed & NM_NDISC_CONFIG_DHCP_LEVEL) {
+        dhcp6_cleanup(self, CLEANUP_TYPE_DECONFIGURE, TRUE);
+
+        priv->dhcp6.mode = rdata->dhcp_level;
+        if (priv->dhcp6.mode != NM_NDISC_DHCP_LEVEL_NONE) {
+            _LOGD(LOGD_DEVICE | LOGD_DHCP6,
+                  "Activation: Stage 3 of 5 (IP Configure Start) starting DHCPv6"
+                  " as requested by IPv6 router...");
+            if (!dhcp6_start(self, FALSE)) {
+                if (priv->dhcp6.mode == NM_NDISC_DHCP_LEVEL_MANAGED) {
+                    nm_device_state_changed(self,
+                                            NM_DEVICE_STATE_FAILED,
+                                            NM_DEVICE_STATE_REASON_DHCP_START_FAILED);
+                    return;
+                }
+            }
+        }
+    }
+
+    if (changed & NM_NDISC_CONFIG_HOP_LIMIT)
+        nm_platform_sysctl_ip_conf_set_ipv6_hop_limit_safe(nm_device_get_platform(self),
+                                                           nm_device_get_ip_iface(self),
+                                                           rdata->hop_limit);
+
+    if (changed & NM_NDISC_CONFIG_REACHABLE_TIME) {
+        nm_platform_sysctl_ip_neigh_set_ipv6_reachable_time(nm_device_get_platform(self),
+                                                            nm_device_get_ip_iface(self),
+                                                            rdata->reachable_time_ms);
+    }
+
+    if (changed & NM_NDISC_CONFIG_RETRANS_TIMER) {
+        nm_platform_sysctl_ip_neigh_set_ipv6_retrans_time(nm_device_get_platform(self),
+                                                          nm_device_get_ip_iface(self),
+                                                          rdata->retrans_timer_ms);
+    }
+
+    if (changed & NM_NDISC_CONFIG_MTU) {
+        if (priv->ip6_mtu != rdata->mtu) {
+            _LOGD(LOGD_DEVICE, "mtu: set IPv6 MTU to %u", (guint) rdata->mtu);
+            priv->ip6_mtu = rdata->mtu;
+        }
+    }
+
+    nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+}
+
+static void
+ndisc_ra_timeout(NMNDisc *ndisc, NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* We don't want to stop listening for router advertisements completely,
+     * but instead let device activation continue activating.  If an RA
+     * shows up later, we'll use it as long as the device is not disconnected.
+     */
+
+    _LOGD(LOGD_IP6, "timed out waiting for IPv6 router advertisement");
+    if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) {
+        /* If RA is our only source of addressing information and we don't
+         * ever receive one, then time out IPv6.  But if there is other
+         * IPv6 configuration, like manual IPv6 addresses or external IPv6
+         * config, consider that sufficient for IPv6 success.
+         *
+         * FIXME: it doesn't seem correct to determine this based on which
+         *        addresses we find inside priv->ip_config_6.
+         */
+        if (priv->ip_config_6
+            && nm_ip6_config_find_first_address(priv->ip_config_6,
+                                                NM_PLATFORM_MATCH_WITH_ADDRTYPE_NORMAL
+                                                    | NM_PLATFORM_MATCH_WITH_ADDRSTATE__ANY))
+            nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+        else
+            nm_device_activate_schedule_ip_config_timeout(self, AF_INET6);
+    }
+}
+
+static void
+addrconf6_start_with_link_ready(NMDevice *self)
+{
+    NMDevicePrivate *  priv = NM_DEVICE_GET_PRIVATE(self);
+    NMUtilsIPv6IfaceId iid;
+
+    g_assert(priv->ndisc);
+
+    if (nm_device_get_ip_iface_identifier(self, &iid, FALSE)) {
+        _LOGD(LOGD_IP6, "addrconf6: using the device EUI-64 identifier");
+        nm_ndisc_set_iid(priv->ndisc, iid);
+    } else {
+        /* Don't abort the addrconf at this point -- if ndisc needs the iid
+         * it will notice this itself. */
+        _LOGI(LOGD_IP6, "addrconf6: no interface identifier; IPv6 address creation may fail");
+    }
+
+    /* Apply any manual configuration before starting RA */
+    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+        _LOGW(LOGD_IP6, "failed to apply manual IPv6 configuration");
+
+    if (nm_ndisc_get_node_type(priv->ndisc) == NM_NDISC_NODE_TYPE_ROUTER) {
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "forwarding", "1");
+        nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+        priv->needs_ip6_subnet = TRUE;
+        g_signal_emit(self, signals[IP6_SUBNET_NEEDED], 0);
+    }
+
+    priv->ndisc_changed_id = g_signal_connect(priv->ndisc,
+                                              NM_NDISC_CONFIG_RECEIVED,
+                                              G_CALLBACK(ndisc_config_changed),
+                                              self);
+    priv->ndisc_timeout_id = g_signal_connect(priv->ndisc,
+                                              NM_NDISC_RA_TIMEOUT_SIGNAL,
+                                              G_CALLBACK(ndisc_ra_timeout),
+                                              self);
+
+    ndisc_set_router_config(priv->ndisc, self);
+    nm_ndisc_start(priv->ndisc);
+    priv->ndisc_started = TRUE;
+    return;
+}
+
+static gboolean
+addrconf6_start(NMDevice *self, NMSettingIP6ConfigPrivacy use_tempaddr)
+{
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *      connection;
+    NMSettingIP6Config *s_ip6 = NULL;
+    GError *            error = NULL;
+    NMUtilsStableType   stable_type;
+    const char *        stable_id;
+    NMNDiscNodeType     node_type;
+    int                 max_addresses;
+    int                 router_solicitations;
+    int                 router_solicitation_interval;
+    guint32             ra_timeout;
+    guint32             default_ra_timeout;
+
+    connection = nm_device_get_applied_connection(self);
+    g_assert(connection);
+
+    nm_assert(!applied_config_get_current(&priv->ac_ip6_config));
+    applied_config_clear(&priv->ac_ip6_config);
+
+    nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref);
+    nm_clear_g_source(&priv->rt6_temporary_not_available_id);
+
+    s_ip6 = NM_SETTING_IP6_CONFIG(nm_connection_get_setting_ip6_config(connection));
+    g_assert(s_ip6);
+
+    if (nm_streq(nm_device_get_effective_ip_config_method(self, AF_INET6),
+                 NM_SETTING_IP4_CONFIG_METHOD_SHARED))
+        node_type = NM_NDISC_NODE_TYPE_ROUTER;
+    else
+        node_type = NM_NDISC_NODE_TYPE_HOST;
+
+    nm_lndp_ndisc_get_sysctl(nm_device_get_platform(self),
+                             nm_device_get_ip_iface(self),
+                             &max_addresses,
+                             &router_solicitations,
+                             &router_solicitation_interval,
+                             &default_ra_timeout);
+
+    if (node_type == NM_NDISC_NODE_TYPE_ROUTER)
+        ra_timeout = 0u;
+    else {
+        ra_timeout = _prop_get_ipv6_ra_timeout(self);
+        if (ra_timeout == 0u)
+            ra_timeout = default_ra_timeout;
+    }
+
+    stable_id   = _prop_get_connection_stable_id(self, connection, &stable_type);
+    priv->ndisc = nm_lndp_ndisc_new(nm_device_get_platform(self),
+                                    nm_device_get_ip_ifindex(self),
+                                    nm_device_get_ip_iface(self),
+                                    stable_type,
+                                    stable_id,
+                                    nm_setting_ip6_config_get_addr_gen_mode(s_ip6),
+                                    node_type,
+                                    max_addresses,
+                                    router_solicitations,
+                                    router_solicitation_interval,
+                                    ra_timeout,
+                                    &error);
+    if (!priv->ndisc) {
+        _LOGE(LOGD_IP6, "addrconf6: failed to start neighbor discovery: %s", error->message);
+        g_error_free(error);
+        return FALSE;
+    }
+
+    priv->ndisc_use_tempaddr = use_tempaddr;
+
+    if (NM_IN_SET(use_tempaddr,
+                  NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR,
+                  NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR)
+        && !nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_EXTENDED_IFA_FLAGS)) {
+        _LOGW(LOGD_IP6,
+              "The kernel does not support extended IFA_FLAGS needed by NM for "
+              "IPv6 private addresses. This feature is not available");
+    }
+
+    /* ensure link local is ready... */
+    if (!linklocal6_start(self)) {
+        /* wait for the LL address to show up */
+        return TRUE;
+    }
+
+    /* already have the LL address; kick off neighbor discovery */
+    addrconf6_start_with_link_ready(self);
+    return TRUE;
+}
+
+static void
+addrconf6_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->ndisc_started = FALSE;
+    nm_clear_g_signal_handler(priv->ndisc, &priv->ndisc_changed_id);
+    nm_clear_g_signal_handler(priv->ndisc, &priv->ndisc_timeout_id);
+
+    applied_config_clear(&priv->ac_ip6_config);
+    nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref);
+    nm_clear_g_source(&priv->rt6_temporary_not_available_id);
+    if (priv->ndisc) {
+        nm_ndisc_stop(priv->ndisc);
+        g_clear_object(&priv->ndisc);
+    }
+}
+
+/*****************************************************************************/
+
+static void
+save_ip6_properties(NMDevice *self)
+{
+    static const char *const ip6_properties_to_save[] = {
+        "accept_ra",
+        "forwarding",
+        "disable_ipv6",
+        "hop_limit",
+        "use_tempaddr",
+    };
+    NMDevicePrivate *priv     = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform *     platform = nm_device_get_platform(self);
+    const char *     ifname;
+    char *           value;
+    int              i;
+
+    g_hash_table_remove_all(priv->ip6_saved_properties);
+
+    ifname = nm_device_get_ip_iface_from_platform(self);
+    if (!ifname)
+        return;
+
+    for (i = 0; i < G_N_ELEMENTS(ip6_properties_to_save); i++) {
+        value =
+            nm_platform_sysctl_ip_conf_get(platform, AF_INET6, ifname, ip6_properties_to_save[i]);
+        if (value) {
+            g_hash_table_insert(priv->ip6_saved_properties,
+                                (char *) ip6_properties_to_save[i],
+                                value);
+        }
+    }
+}
+
+static void
+restore_ip6_properties(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    GHashTableIter   iter;
+    gpointer         key, value;
+
+    g_hash_table_iter_init(&iter, priv->ip6_saved_properties);
+    while (g_hash_table_iter_next(&iter, &key, &value)) {
+        /* Don't touch "disable_ipv6" if we're doing userland IPv6LL */
+        if (priv->ipv6ll_handle && nm_streq(key, "disable_ipv6"))
+            continue;
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, key, value);
+    }
+}
+
+static void
+set_disable_ipv6(NMDevice *self, const char *value)
+{
+    /* We only touch disable_ipv6 when NM is not managing the IPv6LL address */
+    if (!NM_DEVICE_GET_PRIVATE(self)->ipv6ll_handle)
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", value);
+}
+
+static void
+set_nm_ipv6ll(NMDevice *self, gboolean enable)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    int              ifindex = nm_device_get_ip_ifindex(self);
+
+    if (!nm_platform_kernel_support_get(NM_PLATFORM_KERNEL_SUPPORT_TYPE_USER_IPV6LL))
+        return;
+
+    priv->ipv6ll_handle = enable;
+    if (ifindex > 0) {
+        const char *detail = enable ? "enable" : "disable";
+        int         r;
+
+        _LOGD(LOGD_IP6, "will %s userland IPv6LL", detail);
+        r = nm_platform_link_set_user_ipv6ll_enabled(nm_device_get_platform(self), ifindex, enable);
+        if (r < 0) {
+            _NMLOG(NM_IN_SET(r, -NME_PL_NOT_FOUND, -NME_PL_OPNOTSUPP) ? LOGL_DEBUG : LOGL_WARN,
+                   LOGD_IP6,
+                   "failed to %s userspace IPv6LL address handling (%s)",
+                   detail,
+                   nm_strerror(r));
+        }
+
+        if (enable) {
+            gs_free char *value = NULL;
+
+            /* Bounce IPv6 to ensure the kernel stops IPv6LL address generation */
+            value = nm_device_sysctl_ip_conf_get(self, AF_INET6, "disable_ipv6");
+            if (nm_streq0(value, "0"))
+                nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1");
+
+            /* Ensure IPv6 is enabled */
+            nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "0");
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static gboolean
+ip_requires_slaves(NMDevice *self, int addr_family)
+{
+    const char *method;
+
+    method = nm_device_get_effective_ip_config_method(self, addr_family);
+
+    if (NM_IS_IPv4(addr_family))
+        return nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+
+    /* SLAAC, DHCP, and Link-Local depend on connectivity (and thus slaves)
+     * to complete addressing.  SLAAC and DHCP need a peer to provide a prefix.
+     */
+    return NM_IN_STRSET(method,
+                        NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                        NM_SETTING_IP6_CONFIG_METHOD_DHCP);
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           self,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+    NMActStageReturn ret = NM_ACT_STAGE_RETURN_FAILURE;
+    const char *     method;
+
+    nm_assert_addr_family(addr_family);
+
+    connection = nm_device_get_applied_connection(self);
+
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (connection_ip_method_requires_carrier(connection, addr_family, NULL)
+        && nm_device_is_master(self) && !priv->carrier) {
+        _LOGI(LOGD_IP | LOGD_DEVICE,
+              "IPv%c config waiting until carrier is on",
+              nm_utils_addr_family_to_char(addr_family));
+        return NM_ACT_STAGE_RETURN_IP_WAIT;
+    }
+
+    if (nm_device_is_master(self) && ip_requires_slaves(self, addr_family)) {
+        /* If the master has no ready slaves, and depends on slaves for
+         * a successful IP configuration attempt, then postpone IP addressing.
+         */
+        if (!have_any_ready_slaves(self)) {
+            _LOGI(LOGD_DEVICE | LOGD_IP,
+                  "IPv%c config waiting until slaves are ready",
+                  nm_utils_addr_family_to_char(addr_family));
+            return NM_ACT_STAGE_RETURN_IP_WAIT;
+        }
+    }
+
+    if (!IS_IPv4)
+        priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_NONE;
+
+    method = nm_device_get_effective_ip_config_method(self, addr_family);
+
+    _LOGD(LOGD_IP | LOGD_DEVICE,
+          "IPv%c config method is %s",
+          nm_utils_addr_family_to_char(addr_family),
+          method);
+
+    if (IS_IPv4) {
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP4_CONFIG_METHOD_AUTO,
+                         NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) {
+            NMSettingIPConfig *s_ip4;
+            NMIP4Config **     configs, *config;
+            guint              num_addresses;
+
+            s_ip4 = nm_connection_get_setting_ip4_config(connection);
+            g_return_val_if_fail(s_ip4, NM_ACT_STAGE_RETURN_FAILURE);
+            num_addresses = nm_setting_ip_config_get_num_addresses(s_ip4);
+
+            if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+                ret = dhcp4_start(self);
+                if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
+                    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_DHCP_START_FAILED);
+                    return ret;
+                }
+            } else {
+                g_return_val_if_fail(num_addresses != 0, NM_ACT_STAGE_RETURN_FAILURE);
+                ret = NM_ACT_STAGE_RETURN_POSTPONE;
+            }
+
+            if (num_addresses) {
+                config = nm_device_ip4_config_new(self);
+                nm_ip4_config_merge_setting(config,
+                                            nm_connection_get_setting_ip4_config(connection),
+                                            NM_SETTING_CONNECTION_MDNS_DEFAULT,
+                                            NM_SETTING_CONNECTION_LLMNR_DEFAULT,
+                                            nm_device_get_route_table(self, AF_INET),
+                                            nm_device_get_route_metric(self, AF_INET));
+                configs    = g_new0(NMIP4Config *, 2);
+                configs[0] = config;
+                ipv4_dad_start(self, configs, ipv4_manual_method_apply);
+            }
+        } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL)) {
+            ret = ipv4ll_start(self);
+            if (ret == NM_ACT_STAGE_RETURN_FAILURE)
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED);
+        } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED)) {
+            if (out_config) {
+                *out_config = shared4_new_config(self, connection);
+                if (*out_config) {
+                    priv->dnsmasq_manager = nm_dnsmasq_manager_new(nm_device_get_ip_iface(self));
+                    ret                   = NM_ACT_STAGE_RETURN_SUCCESS;
+                } else {
+                    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+                    ret = NM_ACT_STAGE_RETURN_FAILURE;
+                }
+            } else
+                g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE);
+        } else if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
+            ret = NM_ACT_STAGE_RETURN_SUCCESS;
+        else
+            _LOGW(LOGD_IP4, "unhandled IPv4 config method '%s'; will fail", method);
+
+        return ret;
+    } else {
+        NMSettingIP6ConfigPrivacy ip6_privacy     = NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN;
+        const char *              ip6_privacy_str = "0";
+        NMPlatform *              platform;
+        int                       ifindex;
+
+        if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DISABLED)) {
+            nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1");
+            return NM_ACT_STAGE_RETURN_IP_DONE;
+        }
+
+        if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) {
+            if (!nm_device_sys_iface_state_is_external(self)) {
+                if (priv->master) {
+                    /* If a device only has an IPv6 link-local address,
+                     * we don't generate an assumed connection. Therefore,
+                     * when a new slave connection (without IP configuration)
+                     * is activated on the device, the link-local address
+                     * remains configured. The IP configuration of an activated
+                     * slave should not depend on the previous state. Flush
+                     * addresses and routes on activation.
+                     */
+                    ifindex  = nm_device_get_ip_ifindex(self);
+                    platform = nm_device_get_platform(self);
+
+                    if (ifindex > 0) {
+                        gs_unref_object NMIP6Config *config = nm_device_ip6_config_new(self);
+
+                        nm_platform_ip_route_flush(platform, AF_INET6, ifindex);
+                        nm_platform_ip_address_flush(platform, AF_INET6, ifindex);
+                        nm_device_set_ip_config(self, AF_INET6, (NMIPConfig *) config, FALSE, NULL);
+                    }
+                } else {
+                    gboolean ipv6ll_handle_old = priv->ipv6ll_handle;
+
+                    /* When activating an IPv6 'ignore' connection we need to revert back
+                     * to kernel IPv6LL, but the kernel won't actually assign an address
+                     * to the interface until disable_ipv6 is bounced.
+                     */
+                    set_nm_ipv6ll(self, FALSE);
+                    if (ipv6ll_handle_old)
+                        nm_device_sysctl_ip_conf_set(self, AF_INET6, "disable_ipv6", "1");
+                    restore_ip6_properties(self);
+                }
+            }
+            return NM_ACT_STAGE_RETURN_IP_DONE;
+        }
+
+        /* Ensure the MTU makes sense. If it was below 1280 the kernel would not
+         * expose any ipv6 sysctls or allow presence of any addresses on the interface,
+         * including LL, which * would make it impossible to autoconfigure MTU to a
+         * correct value. */
+        _commit_mtu(self, priv->ip_config_4);
+
+        /* Any method past this point requires an IPv6LL address. Use NM-controlled
+         * IPv6LL if this is not an assumed connection, since assumed connections
+         * will already have IPv6 set up.
+         */
+        if (!nm_device_sys_iface_state_is_external_or_assume(self))
+            set_nm_ipv6ll(self, TRUE);
+
+        /* Re-enable IPv6 on the interface */
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "0");
+        set_disable_ipv6(self, "0");
+
+        /* Synchronize external IPv6 configuration with kernel, since
+         * linklocal6_start() uses the information there to determine if we can
+         * proceed with the selected method (SLAAC, DHCP, link-local).
+         */
+        nm_platform_process_events(nm_device_get_platform(self));
+        g_clear_object(&priv->ext_ip6_config_captured);
+        priv->ext_ip6_config_captured =
+            nm_ip6_config_capture(nm_device_get_multi_index(self),
+                                  nm_device_get_platform(self),
+                                  nm_device_get_ip_ifindex(self),
+                                  NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+
+        ip6_privacy = _prop_get_ipv6_ip6_privacy(self);
+
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                         NM_SETTING_IP6_CONFIG_METHOD_SHARED)) {
+            if (!addrconf6_start(self, ip6_privacy)) {
+                /* IPv6 might be disabled; allow IPv4 to proceed */
+                ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+            } else
+                ret = NM_ACT_STAGE_RETURN_POSTPONE;
+        } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL)) {
+            ret =
+                linklocal6_start(self) ? NM_ACT_STAGE_RETURN_SUCCESS : NM_ACT_STAGE_RETURN_POSTPONE;
+        } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_DHCP)) {
+            priv->dhcp6.mode = NM_NDISC_DHCP_LEVEL_MANAGED;
+            if (!dhcp6_start(self, TRUE)) {
+                /* IPv6 might be disabled; allow IPv4 to proceed */
+                ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+            } else
+                ret = NM_ACT_STAGE_RETURN_POSTPONE;
+        } else if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
+            ret = NM_ACT_STAGE_RETURN_SUCCESS;
+        else
+            _LOGW(LOGD_IP6, "unhandled IPv6 config method '%s'; will fail", method);
+
+        if (ret != NM_ACT_STAGE_RETURN_FAILURE
+            && !nm_device_sys_iface_state_is_external_or_assume(self)) {
+            switch (ip6_privacy) {
+            case NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN:
+            case NM_SETTING_IP6_CONFIG_PRIVACY_DISABLED:
+                ip6_privacy_str = "0";
+                break;
+            case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_PUBLIC_ADDR:
+                ip6_privacy_str = "1";
+                break;
+            case NM_SETTING_IP6_CONFIG_PRIVACY_PREFER_TEMP_ADDR:
+                ip6_privacy_str = "2";
+                break;
+            }
+            nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", ip6_privacy_str);
+        }
+
+        return ret;
+    }
+}
+
+gboolean
+nm_device_activate_stage3_ip_start(NMDevice *self, int addr_family)
+{
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *   priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMActStageReturn    ret;
+    NMDeviceStateReason failure_reason    = NM_DEVICE_STATE_REASON_NONE;
+    gs_unref_object NMIPConfig *ip_config = NULL;
+
+    g_assert(priv->ip_state_x[IS_IPv4] == NM_DEVICE_IP_STATE_WAIT);
+
+    if (nm_device_sys_iface_state_is_external(self)) {
+        _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE);
+        check_ip_state(self, FALSE, TRUE);
+        return TRUE;
+    }
+
+    _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_CONF);
+
+    ret = NM_DEVICE_GET_CLASS(self)->act_stage3_ip_config_start(self,
+                                                                addr_family,
+                                                                (gpointer *) &ip_config,
+                                                                &failure_reason);
+
+    switch (ret) {
+    case NM_ACT_STAGE_RETURN_SUCCESS:
+        if (!IS_IPv4) {
+            /* Here we get a static IPv6 config, like for Shared where it's
+             * autogenerated or from modems where it comes from ModemManager.
+            */
+            if (!ip_config)
+                ip_config = nm_device_ip_config_new(self, addr_family);
+            nm_assert(!applied_config_get_current(&priv->ac_ip6_config));
+            applied_config_init(&priv->ac_ip6_config, ip_config);
+            ip_config = NULL;
+        }
+        nm_device_activate_schedule_ip_config_result(self, addr_family, ip_config);
+        break;
+    case NM_ACT_STAGE_RETURN_IP_DONE:
+        _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE);
+        check_ip_state(self, FALSE, TRUE);
+        break;
+    case NM_ACT_STAGE_RETURN_FAILURE:
+        nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
+        return FALSE;
+    case NM_ACT_STAGE_RETURN_IP_FAIL:
+        /* Activation not wanted */
+        _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL);
+        break;
+    case NM_ACT_STAGE_RETURN_IP_WAIT:
+        /* Wait for something to try IP config again */
+        _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_WAIT);
+        break;
+    default:
+        g_assert(ret == NM_ACT_STAGE_RETURN_POSTPONE);
+    }
+
+    return TRUE;
+}
+
+/*
+ * activate_stage3_ip_config_start
+ *
+ * Begin automatic/manual IP configuration
+ *
+ */
+static void
+activate_stage3_ip_config_start(NMDevice *self)
+{
+    int ifindex;
+
+    _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_WAIT);
+    _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_WAIT);
+
+    _active_connection_set_state_flags(self, NM_ACTIVATION_STATE_FLAG_LAYER2_READY);
+
+    nm_device_state_changed(self, NM_DEVICE_STATE_IP_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+
+    /* Device should be up before we can do anything with it */
+    if ((ifindex = nm_device_get_ip_ifindex(self)) > 0
+        && !nm_platform_link_is_up(nm_device_get_platform(self), ifindex))
+        _LOGW(LOGD_DEVICE,
+              "interface %s not up for IP configuration",
+              nm_device_get_ip_iface(self));
+
+    if (nm_device_activate_ip4_state_in_wait(self)
+        && !nm_device_activate_stage3_ip_start(self, AF_INET))
+        return;
+
+    if (nm_device_activate_ip6_state_in_wait(self)
+        && !nm_device_activate_stage3_ip_start(self, AF_INET6))
+        return;
+
+    /* Proxy */
+    nm_device_set_proxy_config(self, NULL);
+
+    check_ip_state(self, TRUE, TRUE);
+}
+
+static void
+fw_change_zone_cb(NMFirewallManager *      firewall_manager,
+                  NMFirewallManagerCallId *call_id,
+                  GError *                 error,
+                  gpointer                 user_data)
+{
+    NMDevice *       self = user_data;
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->fw_call != call_id)
+        g_return_if_reached();
+
+    priv->fw_call = NULL;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    switch (priv->fw_state) {
+    case FIREWALL_STATE_WAIT_STAGE_3:
+        priv->fw_state = FIREWALL_STATE_INITIALIZED;
+        nm_device_activate_schedule_stage3_ip_config_start(self);
+        break;
+    case FIREWALL_STATE_WAIT_IP_CONFIG:
+        priv->fw_state = FIREWALL_STATE_INITIALIZED;
+        if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE
+            || priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE)
+            nm_device_start_ip_check(self);
+        break;
+    case FIREWALL_STATE_INITIALIZED:
+        break;
+    default:
+        g_return_if_reached();
+    }
+}
+
+static void
+fw_change_zone(NMDevice *self)
+{
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *       applied_connection;
+    NMSettingConnection *s_con;
+    const char *         zone;
+
+    nm_assert(priv->fw_state >= FIREWALL_STATE_INITIALIZED);
+
+    applied_connection = nm_device_get_applied_connection(self);
+    nm_assert(applied_connection);
+
+    s_con = nm_connection_get_setting_connection(applied_connection);
+    nm_assert(s_con);
+
+    if (priv->fw_call) {
+        nm_firewall_manager_cancel_call(priv->fw_call);
+        nm_assert(!priv->fw_call);
+    }
+
+    if (G_UNLIKELY(!priv->fw_mgr))
+        priv->fw_mgr = g_object_ref(nm_firewall_manager_get());
+
+    zone = nm_setting_connection_get_zone(s_con);
+#if WITH_FIREWALLD_ZONE
+    if (!zone || zone[0] == '\0') {
+        if (nm_streq0(nm_device_get_effective_ip_config_method(self, AF_INET),
+                      NM_SETTING_IP4_CONFIG_METHOD_SHARED)
+            || nm_streq0(nm_device_get_effective_ip_config_method(self, AF_INET6),
+                         NM_SETTING_IP6_CONFIG_METHOD_SHARED))
+            zone = "nm-shared";
+    }
+#endif
+    priv->fw_call = nm_firewall_manager_add_or_change_zone(priv->fw_mgr,
+                                                           nm_device_get_ip_iface(self),
+                                                           zone,
+                                                           FALSE, /* change zone */
+                                                           fw_change_zone_cb,
+                                                           self);
+}
+
+/*
+ * nm_device_activate_schedule_stage3_ip_config_start
+ *
+ * Schedule IP configuration start
+ */
+void
+nm_device_activate_schedule_stage3_ip_config_start(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+    int              ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    g_return_if_fail(priv->act_request.obj);
+    ifindex = nm_device_get_ip_ifindex(self);
+
+    /* Add the interface to the specified firewall zone */
+    if (priv->fw_state == FIREWALL_STATE_UNMANAGED) {
+        if (nm_device_sys_iface_state_is_external(self)) {
+            /* fake success */
+            priv->fw_state = FIREWALL_STATE_INITIALIZED;
+        } else if (ifindex > 0) {
+            priv->fw_state = FIREWALL_STATE_WAIT_STAGE_3;
+            fw_change_zone(self);
+            return;
+        }
+        /* no ifindex, nothing to do for now */
+    } else if (priv->fw_state == FIREWALL_STATE_WAIT_STAGE_3) {
+        /* a firewall call for stage3 is pending. Return and wait. */
+        return;
+    }
+
+    nm_assert(ifindex <= 0 || priv->fw_state == FIREWALL_STATE_INITIALIZED);
+
+    activation_source_schedule(self, activate_stage3_ip_config_start, AF_INET);
+}
+
+static NMActStageReturn
+act_stage4_ip_config_timeout(NMDevice *           self,
+                             int                  addr_family,
+                             NMDeviceStateReason *out_failure_reason)
+{
+    nm_assert_addr_family(addr_family);
+
+    if (!get_ip_config_may_fail(self, addr_family)) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+activate_stage4_ip_config_timeout_x(NMDevice *self, int addr_family)
+{
+    NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_NONE;
+    NMActStageReturn    ret;
+
+    ret =
+        NM_DEVICE_GET_CLASS(self)->act_stage4_ip_config_timeout(self, addr_family, &failure_reason);
+
+    if (ret == NM_ACT_STAGE_RETURN_POSTPONE)
+        return;
+
+    if (ret == NM_ACT_STAGE_RETURN_FAILURE) {
+        nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
+        return;
+    }
+    g_assert(ret == NM_ACT_STAGE_RETURN_SUCCESS);
+
+    _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_FAIL);
+    check_ip_state(self, FALSE, TRUE);
+}
+
+static void
+activate_stage4_ip_config_timeout_4(NMDevice *self)
+{
+    activate_stage4_ip_config_timeout_x(self, AF_INET);
+}
+
+static void
+activate_stage4_ip_config_timeout_6(NMDevice *self)
+{
+    activate_stage4_ip_config_timeout_x(self, AF_INET6);
+}
+
+void
+nm_device_activate_schedule_ip_config_timeout(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->act_request.obj);
+
+    activation_source_schedule(self,
+                               IS_IPv4 ? activate_stage4_ip_config_timeout_4
+                                       : activate_stage4_ip_config_timeout_6,
+                               addr_family);
+}
+
+static gboolean
+share_init(NMDevice *self, GError **error)
+{
+    const char *const modules[] = {"ip_tables",
+                                   "iptable_nat",
+                                   "nf_nat_ftp",
+                                   "nf_nat_irc",
+                                   "nf_nat_sip",
+                                   "nf_nat_tftp",
+                                   "nf_nat_pptp",
+                                   "nf_nat_h323"};
+    guint             i;
+    int               errsv;
+
+    if (nm_platform_sysctl_get_int32(nm_device_get_platform(self),
+                                     NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"),
+                                     -1)
+        == 1) {
+        /* nothing to do. */
+    } else if (!nm_platform_sysctl_set(nm_device_get_platform(self),
+                                       NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"),
+                                       "1")) {
+        errsv = errno;
+        _LOGD(LOGD_SHARING,
+              "share: error enabling IPv4 forwarding: (%d) %s",
+              errsv,
+              nm_strerror_native(errsv));
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "cannot set ipv4/ip_forward: %s",
+                    nm_strerror_native(errsv));
+        return FALSE;
+    }
+
+    if (nm_platform_sysctl_get_int32(nm_device_get_platform(self),
+                                     NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_dynaddr"),
+                                     -1)
+        == 1) {
+        /* nothing to do. */
+    } else if (!nm_platform_sysctl_set(nm_device_get_platform(self),
+                                       NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_dynaddr"),
+                                       "1")) {
+        errsv = errno;
+        _LOGD(LOGD_SHARING,
+              "share: error enabling dynamic addresses: (%d) %s",
+              errsv,
+              nm_strerror_native(errsv));
+    }
+
+    for (i = 0; i < G_N_ELEMENTS(modules); i++)
+        nm_utils_modprobe(NULL, FALSE, modules[i], NULL);
+
+    return TRUE;
+}
+
+static gboolean
+start_sharing(NMDevice *self, NMIP4Config *config, GError **error)
+{
+    NMDevicePrivate *           priv = NM_DEVICE_GET_PRIVATE(self);
+    NMActRequest *              req;
+    const NMPlatformIP4Address *ip4_addr = NULL;
+    const char *                ip_iface;
+    GError *                    local = NULL;
+    NMConnection *              conn;
+    NMSettingConnection *       s_con;
+    gboolean                    announce_android_metered;
+    NMUtilsShareRules *         share_rules;
+
+    g_return_val_if_fail(config, FALSE);
+
+    ip_iface = nm_device_get_ip_iface(self);
+    if (!ip_iface) {
+        g_set_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "device has no ip interface");
+        return FALSE;
+    }
+
+    ip4_addr = nm_ip4_config_get_first_address(config);
+    if (!ip4_addr || !ip4_addr->address) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "could not determine IPv4 address");
+        return FALSE;
+    }
+
+    if (!share_init(self, error))
+        return FALSE;
+
+    req = nm_device_get_act_request(self);
+    g_return_val_if_fail(req, FALSE);
+
+    share_rules = nm_utils_share_rules_new();
+
+    nm_utils_share_rules_add_all_rules(share_rules, ip_iface, ip4_addr->address, ip4_addr->plen);
+
+    nm_utils_share_rules_apply(share_rules, TRUE);
+
+    nm_act_request_set_shared(req, share_rules);
+
+    conn  = nm_act_request_get_applied_connection(req);
+    s_con = nm_connection_get_setting_connection(conn);
+
+    switch (nm_setting_connection_get_metered(s_con)) {
+    case NM_METERED_YES:
+        /* honor the metered flag. Note that reapply on the device does not affect
+         * the metered setting. This is different from other profiles, where the
+         * metered flag of an activated profile can be changed (reapplied). */
+        announce_android_metered = TRUE;
+        break;
+    case NM_METERED_UNKNOWN:
+        /* we pick up the current value and announce it. But again, we cannot update
+         * the announced setting without restarting dnsmasq. That means, if the default
+         * route changes w.r.t. being metered, then the shared connection does not get
+         * updated before reactivating. */
+        announce_android_metered =
+            NM_IN_SET(nm_manager_get_metered(NM_MANAGER_GET), NM_METERED_YES, NM_METERED_GUESS_YES);
+        break;
+    default:
+        announce_android_metered = FALSE;
+        break;
+    }
+
+    if (!nm_dnsmasq_manager_start(priv->dnsmasq_manager,
+                                  config,
+                                  announce_android_metered,
+                                  &local)) {
+        g_set_error(error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "could not start dnsmasq due to %s",
+                    local->message);
+        g_error_free(local);
+        nm_act_request_set_shared(req, NULL);
+        return FALSE;
+    }
+
+    priv->dnsmasq_state_id = g_signal_connect(priv->dnsmasq_manager,
+                                              NM_DNS_MASQ_MANAGER_STATE_CHANGED,
+                                              G_CALLBACK(dnsmasq_state_changed_cb),
+                                              self);
+    return TRUE;
+}
+
+static void
+arp_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->acd.announcing, nm_acd_manager_free);
+}
+
+void
+nm_device_arp_announce(NMDevice *self)
+{
+    NMDevicePrivate *  priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *     connection;
+    NMSettingIPConfig *s_ip4;
+    guint              num, i;
+    const guint8 *     hw_addr;
+    size_t             hw_addr_len = 0;
+
+    arp_cleanup(self);
+
+    hw_addr = nm_platform_link_get_address(nm_device_get_platform(self),
+                                           nm_device_get_ip_ifindex(self),
+                                           &hw_addr_len);
+
+    if (!hw_addr || hw_addr_len != ETH_ALEN)
+        return;
+
+    /* We only care about manually-configured addresses; DHCP- and autoip-configured
+     * ones should already have been seen on the network at this point.
+     */
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        return;
+    s_ip4 = nm_connection_get_setting_ip4_config(connection);
+    if (!s_ip4)
+        return;
+    num = nm_setting_ip_config_get_num_addresses(s_ip4);
+    if (num == 0)
+        return;
+
+    priv->acd.announcing =
+        nm_acd_manager_new(nm_device_get_ip_ifindex(self), hw_addr, hw_addr_len, NULL, NULL);
+
+    for (i = 0; i < num; i++) {
+        NMIPAddress *ip = nm_setting_ip_config_get_address(s_ip4, i);
+        in_addr_t    addr;
+
+        if (inet_pton(AF_INET, nm_ip_address_get_address(ip), &addr) == 1)
+            nm_acd_manager_add_address(priv->acd.announcing, addr);
+        else
+            g_warn_if_reached();
+    }
+
+    nm_acd_manager_announce_addresses(priv->acd.announcing);
+}
+
+static void
+activate_stage5_ip_config_result_x(NMDevice *self, int addr_family)
+{
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMActRequest *   req;
+    const char *     method;
+    int              ip_ifindex;
+    int              errsv;
+    gboolean         do_announce = FALSE;
+
+    req = nm_device_get_act_request(self);
+    g_assert(req);
+
+    /* Interface must be IFF_UP before IP config can be applied */
+    ip_ifindex = nm_device_get_ip_ifindex(self);
+    g_return_if_fail(ip_ifindex);
+
+    if (!nm_platform_link_is_up(nm_device_get_platform(self), ip_ifindex)
+        && !nm_device_sys_iface_state_is_external_or_assume(self)) {
+        nm_platform_link_set_up(nm_device_get_platform(self), ip_ifindex, NULL);
+        if (!nm_platform_link_is_up(nm_device_get_platform(self), ip_ifindex))
+            _LOGW(LOGD_DEVICE,
+                  "interface %s not up for IP configuration",
+                  nm_device_get_ip_iface(self));
+    }
+
+    if (!ip_config_merge_and_apply(self, addr_family, TRUE)) {
+        _LOGD(LOGD_DEVICE | LOGD_IPX(IS_IPv4),
+              "Activation: Stage 5 of 5 (IPv%c Commit) failed",
+              nm_utils_addr_family_to_char(addr_family));
+        nm_device_ip_method_failed(self, addr_family, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return;
+    }
+
+    if (!IS_IPv4) {
+        if (priv->dhcp6.mode != NM_NDISC_DHCP_LEVEL_NONE
+            && priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF) {
+            if (applied_config_get_current(&priv->dhcp6.ip6_config)) {
+                /* If IPv6 wasn't the first IP to complete, and DHCP was used,
+                 * then ensure dispatcher scripts get the DHCP lease information.
+                 */
+                nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP6_CHANGE,
+                                          self,
+                                          NULL,
+                                          NULL,
+                                          NULL,
+                                          NULL);
+            } else {
+                /* still waiting for first dhcp6 lease. */
+                return;
+            }
+        }
+    }
+
+    /* Start IPv4 sharing/IPv6 forwarding if we need it */
+    method = nm_device_get_effective_ip_config_method(self, addr_family);
+    if (IS_IPv4) {
+        if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_SHARED)) {
+            gs_free_error GError *error = NULL;
+
+            if (!start_sharing(self, priv->ip_config_4, &error)) {
+                _LOGW(LOGD_SHARING,
+                      "Activation: Stage 5 of 5 (IPv4 Commit) start sharing failed: %s",
+                      error->message);
+                nm_device_ip_method_failed(self,
+                                           AF_INET,
+                                           NM_DEVICE_STATE_REASON_SHARED_START_FAILED);
+                return;
+            }
+        }
+    } else {
+        if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_SHARED)) {
+            if (!nm_platform_sysctl_set(
+                    nm_device_get_platform(self),
+                    NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv6/conf/all/forwarding"),
+                    "1")) {
+                errsv = errno;
+                _LOGE(LOGD_SHARING,
+                      "share: error enabling IPv6 forwarding: (%d) %s",
+                      errsv,
+                      nm_strerror_native(errsv));
+                nm_device_ip_method_failed(self,
+                                           AF_INET6,
+                                           NM_DEVICE_STATE_REASON_SHARED_START_FAILED);
+                return;
+            }
+        }
+    }
+
+    if (IS_IPv4) {
+        if (priv->dhcp_data_4.client) {
+            gs_free_error GError *error = NULL;
+
+            if (!nm_dhcp_client_accept(priv->dhcp_data_4.client, &error)) {
+                _LOGW(LOGD_DHCP4,
+                      "Activation: Stage 5 of 5 (IPv4 Commit) error accepting lease: %s",
+                      error->message);
+                nm_device_ip_method_failed(self, AF_INET, NM_DEVICE_STATE_REASON_DHCP_ERROR);
+                return;
+            }
+        }
+
+        /* If IPv4 wasn't the first to complete, and DHCP was used, then ensure
+         * dispatcher scripts get the DHCP lease information.
+         */
+        if (priv->dhcp_data_4.client && nm_device_activate_ip4_state_in_conf(self)
+            && (nm_device_get_state(self) > NM_DEVICE_STATE_IP_CONFIG)) {
+            nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DHCP4_CHANGE,
+                                      self,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      NULL);
+        }
+    }
+
+    if (!IS_IPv4) {
+        /* Check if we have to wait for DAD */
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF && !priv->dad6_ip6_config) {
+            if (!priv->carrier && priv->ignore_carrier && get_ip_config_may_fail(self, AF_INET6))
+                _LOGI(LOGD_DEVICE | LOGD_IP6,
+                      "IPv6 DAD: carrier missing and ignored, not delaying activation");
+            else
+                priv->dad6_ip6_config = dad6_get_pending_addresses(self);
+
+            if (priv->dad6_ip6_config) {
+                _LOGD(LOGD_DEVICE | LOGD_IP6, "IPv6 DAD: awaiting termination");
+            } else {
+                _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_DONE);
+                check_ip_state(self, FALSE, TRUE);
+            }
+        }
+    }
+
+    if (IS_IPv4) {
+        /* Send ARP announcements */
+
+        if (nm_device_is_master(self)) {
+            CList *    iter;
+            SlaveInfo *info;
+
+            /* Skip announcement if there are no device enslaved, for two reasons:
+             * 1) the master has a temporary MAC address until the first slave comes
+             * 2) announcements are going to be dropped anyway without slaves
+             */
+            do_announce = FALSE;
+
+            c_list_for_each (iter, &priv->slaves) {
+                info = c_list_entry(iter, SlaveInfo, lst_slave);
+                if (info->slave_is_enslaved) {
+                    do_announce = TRUE;
+                    break;
+                }
+            }
+        } else
+            do_announce = TRUE;
+
+        if (do_announce)
+            nm_device_arp_announce(self);
+    }
+
+    if (IS_IPv4) {
+        /* Enter the IP_CHECK state if this is the first method to complete */
+        _set_ip_state(self, AF_INET, NM_DEVICE_IP_STATE_DONE);
+        check_ip_state(self, FALSE, TRUE);
+    }
+}
+
+static void
+activate_stage5_ip_config_result_4(NMDevice *self)
+{
+    activate_stage5_ip_config_result_x(self, AF_INET);
+}
+
+static void
+activate_stage5_ip_config_result_6(NMDevice *self)
+{
+    activate_stage5_ip_config_result_x(self, AF_INET6);
+}
+
+#define activate_stage5_ip_config_result_x_fcn(addr_family)       \
+    (NM_IS_IPv4(addr_family) ? activate_stage5_ip_config_result_4 \
+                             : activate_stage5_ip_config_result_6)
+
+void
+nm_device_activate_schedule_ip_config_result(NMDevice *self, int addr_family, NMIPConfig *config)
+{
+    NMDevicePrivate *priv;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(!config || (IS_IPv4 && nm_ip_config_get_addr_family(config) == AF_INET));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (IS_IPv4) {
+        applied_config_init(&priv->dev_ip_config_4, config);
+    } else {
+        /* If IP had previously failed, move it back to NM_DEVICE_IP_STATE_CONF since we
+         * clearly now have configuration.
+         */
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_FAIL)
+            _set_ip_state(self, AF_INET6, NM_DEVICE_IP_STATE_CONF);
+    }
+
+    activation_source_schedule(self,
+                               activate_stage5_ip_config_result_x_fcn(addr_family),
+                               addr_family);
+}
+
+NMDeviceIPState
+nm_device_activate_get_ip_state(NMDevice *self, int addr_family)
+{
+    const int IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_IP_STATE_NONE);
+    g_return_val_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6), NM_DEVICE_IP_STATE_NONE);
+
+    return NM_DEVICE_GET_PRIVATE(self)->ip_state_x[IS_IPv4];
+}
+
+static void
+dad6_add_pending_address(NMDevice *             self,
+                         NMPlatform *           platform,
+                         int                    ifindex,
+                         const struct in6_addr *address,
+                         NMIP6Config **         dad6_config)
+{
+    const NMPlatformIP6Address *pl_addr;
+
+    pl_addr = nm_platform_ip6_address_get(platform, ifindex, address);
+    if (pl_addr && NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_TENTATIVE)
+        && !NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_DADFAILED)
+        && !NM_FLAGS_HAS(pl_addr->n_ifa_flags, IFA_F_OPTIMISTIC)) {
+        _LOGt(LOGD_DEVICE,
+              "IPv6 DAD: pending address %s",
+              nm_platform_ip6_address_to_string(pl_addr, NULL, 0));
+
+        if (!*dad6_config)
+            *dad6_config = nm_device_ip6_config_new(self);
+
+        nm_ip6_config_add_address(*dad6_config, pl_addr);
+    }
+}
+
+/*
+ * Returns a NMIP6Config containing NM-configured addresses which
+ * have the tentative flag, or NULL if none is present.
+ */
+static NMIP6Config *
+dad6_get_pending_addresses(NMDevice *self)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    NMIP6Config *    confs[] = {(NMIP6Config *) applied_config_get_current(&priv->ac_ip6_config),
+                            (NMIP6Config *) applied_config_get_current(&priv->dhcp6.ip6_config),
+                            priv->con_ip_config_6,
+                            (NMIP6Config *) applied_config_get_current(&priv->dev2_ip_config_6)};
+    const NMPlatformIP6Address *addr;
+    NMIP6Config *               dad6_config = NULL;
+    NMDedupMultiIter            ipconf_iter;
+    guint                       i;
+    int                         ifindex;
+    NMPlatform *                platform;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    g_return_val_if_fail(ifindex > 0, NULL);
+
+    platform = nm_device_get_platform(self);
+
+    if (priv->ipv6ll_has) {
+        dad6_add_pending_address(self, platform, ifindex, &priv->ipv6ll_addr, &dad6_config);
+    }
+
+    /* We are interested only in addresses that we have explicitly configured,
+     * not in externally added ones.
+     */
+    for (i = 0; i < G_N_ELEMENTS(confs); i++) {
+        if (!confs[i])
+            continue;
+
+        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, confs[i], &addr) {
+            dad6_add_pending_address(self, platform, ifindex, &addr->address, &dad6_config);
+        }
+    }
+
+    return dad6_config;
+}
+
+/*****************************************************************************/
+
+static void
+act_request_set(NMDevice *self, NMActRequest *act_request)
+{
+    NMDevicePrivate *priv;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(!act_request || NM_IS_ACT_REQUEST(act_request));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->act_request.visible && priv->act_request.obj == act_request)
+        return;
+
+    /* always clear the public flag. The few callers that set a new @act_request
+     * don't want that the property is public yet.  */
+    nm_dbus_track_obj_path_set(&priv->act_request, act_request, FALSE);
+
+    if (act_request) {
+        switch (nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(act_request))) {
+        case NM_ACTIVATION_TYPE_EXTERNAL:
+            break;
+        case NM_ACTIVATION_TYPE_ASSUME:
+            if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL)
+                nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_ASSUME);
+            break;
+        case NM_ACTIVATION_TYPE_MANAGED:
+            if (NM_IN_SET_TYPED(NMDeviceSysIfaceState,
+                                priv->sys_iface_state,
+                                NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+                                NM_DEVICE_SYS_IFACE_STATE_ASSUME))
+                nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED);
+            break;
+        }
+    }
+}
+
+static void
+dnsmasq_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->dnsmasq_manager)
+        return;
+
+    nm_clear_g_signal_handler(priv->dnsmasq_manager, &priv->dnsmasq_state_id);
+
+    nm_dnsmasq_manager_stop(priv->dnsmasq_manager);
+    g_object_unref(priv->dnsmasq_manager);
+    priv->dnsmasq_manager = NULL;
+}
+
+gboolean
+nm_device_is_nm_owned(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->nm_owned;
+}
+
+/*
+ * delete_on_deactivate_link_delete
+ *
+ * Function will be queued with g_idle_add to call
+ * nm_platform_link_delete for the underlying resources
+ * of the device.
+ */
+static gboolean
+delete_on_deactivate_link_delete(gpointer user_data)
+{
+    DeleteOnDeactivateData *data = user_data;
+    NMDevice *              self = data->device;
+
+    _LOGD(LOGD_DEVICE,
+          "delete_on_deactivate: cleanup and delete virtual link #%d (id=%u)",
+          data->ifindex,
+          data->idle_add_id);
+
+    if (data->device) {
+        NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(data->device);
+        gs_free_error GError *error = NULL;
+
+        g_object_remove_weak_pointer(G_OBJECT(data->device), (void **) &data->device);
+        priv->delete_on_deactivate_data = NULL;
+
+        if (!nm_device_unrealize(data->device, TRUE, &error))
+            _LOGD(LOGD_DEVICE,
+                  "delete_on_deactivate: unrealizing %d failed (%s)",
+                  data->ifindex,
+                  error->message);
+    } else if (data->ifindex > 0)
+        nm_platform_link_delete(nm_device_get_platform(self), data->ifindex);
+
+    nm_device_emit_recheck_auto_activate(self);
+
+    g_free(data);
+    return FALSE;
+}
+
+static void
+delete_on_deactivate_unschedule(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->delete_on_deactivate_data) {
+        DeleteOnDeactivateData *data = priv->delete_on_deactivate_data;
+
+        priv->delete_on_deactivate_data = NULL;
+
+        g_source_remove(data->idle_add_id);
+        g_object_remove_weak_pointer(G_OBJECT(self), (void **) &data->device);
+        _LOGD(LOGD_DEVICE,
+              "delete_on_deactivate: cancel cleanup and delete virtual link #%d (id=%u)",
+              data->ifindex,
+              data->idle_add_id);
+        g_free(data);
+    }
+}
+
+static void
+delete_on_deactivate_check_and_schedule(NMDevice *self, int ifindex)
+{
+    NMDevicePrivate *       priv = NM_DEVICE_GET_PRIVATE(self);
+    DeleteOnDeactivateData *data;
+
+    if (!priv->nm_owned)
+        return;
+    if (priv->queued_act_request)
+        return;
+    if (!nm_device_is_software(self) || !nm_device_is_real(self))
+        return;
+    if (nm_device_get_state(self) == NM_DEVICE_STATE_UNMANAGED)
+        return;
+    if (nm_device_get_state(self) == NM_DEVICE_STATE_UNAVAILABLE)
+        return;
+    delete_on_deactivate_unschedule(self); /* always cancel and reschedule */
+
+    data = g_new(DeleteOnDeactivateData, 1);
+    g_object_add_weak_pointer(G_OBJECT(self), (void **) &data->device);
+    data->device                    = self;
+    data->ifindex                   = ifindex;
+    data->idle_add_id               = g_idle_add(delete_on_deactivate_link_delete, data);
+    priv->delete_on_deactivate_data = data;
+
+    _LOGD(LOGD_DEVICE,
+          "delete_on_deactivate: schedule cleanup and delete virtual link #%d (id=%u)",
+          ifindex,
+          data->idle_add_id);
+}
+
+static void
+_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type)
+{
+    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_NONE);
+
+    if (nm_clear_g_source(&priv->queued_ip_config_id_x[IS_IPv4])) {
+        _LOGD(LOGD_DEVICE,
+              "clearing queued IP%c config change",
+              nm_utils_addr_family_to_char(addr_family));
+    }
+
+    if (IS_IPv4) {
+        dhcp4_cleanup(self, cleanup_type, FALSE);
+        arp_cleanup(self);
+        dnsmasq_cleanup(self);
+        ipv4ll_cleanup(self);
+        g_slist_free_full(priv->acd.dad_list, (GDestroyNotify) nm_acd_manager_free);
+        priv->acd.dad_list = NULL;
+    } else {
+        g_slist_free_full(priv->dad6_failed_addrs, (GDestroyNotify) nmp_object_unref);
+        priv->dad6_failed_addrs = NULL;
+        g_clear_object(&priv->dad6_ip6_config);
+        dhcp6_cleanup(self, cleanup_type, FALSE);
+        nm_clear_g_source(&priv->linklocal6_timeout_id);
+        addrconf6_cleanup(self);
+    }
+}
+
+gboolean
+_nm_device_hash_check_invalid_keys(GHashTable *       hash,
+                                   const char *       setting_name,
+                                   GError **          error,
+                                   const char *const *whitelist)
+{
+    guint found_whitelisted_keys = 0;
+    guint i;
+
+    nm_assert(hash && g_hash_table_size(hash) > 0);
+    nm_assert(whitelist && whitelist[0]);
+
+#if NM_MORE_ASSERTS > 10
+    /* Require whitelist to only contain unique keys. */
+    {
+        gs_unref_hashtable GHashTable *check_dups =
+            g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, NULL);
+
+        for (i = 0; whitelist[i]; i++) {
+            if (!g_hash_table_add(check_dups, (char *) whitelist[i]))
+                nm_assert(FALSE);
+        }
+        nm_assert(g_hash_table_size(check_dups) > 0);
+    }
+#endif
+
+    for (i = 0; whitelist[i]; i++) {
+        if (g_hash_table_contains(hash, whitelist[i]))
+            found_whitelisted_keys++;
+    }
+
+    if (found_whitelisted_keys == g_hash_table_size(hash)) {
+        /* Good, there are only whitelisted keys in the hash. */
+        return TRUE;
+    }
+
+    if (error) {
+        GHashTableIter iter;
+        const char *   k                 = NULL;
+        const char *   first_invalid_key = NULL;
+
+        g_hash_table_iter_init(&iter, hash);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &k, NULL)) {
+            if (nm_utils_strv_find_first((char **) whitelist, -1, k) < 0) {
+                first_invalid_key = k;
+                break;
+            }
+        }
+        if (setting_name) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                        "Can't reapply changes to '%s.%s' setting",
+                        setting_name,
+                        first_invalid_key);
+        } else {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                        "Can't reapply any changes to '%s' setting",
+                        first_invalid_key);
+        }
+        g_return_val_if_fail(first_invalid_key, FALSE);
+    }
+
+    return FALSE;
+}
+
+void
+nm_device_reactivate_ip_config(NMDevice *         self,
+                               int                addr_family,
+                               NMSettingIPConfig *s_ip_old,
+                               NMSettingIPConfig *s_ip_new)
+{
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv;
+    const char *     method_old;
+    const char *     method_new;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->ip_state_x[IS_IPv4] == NM_DEVICE_IP_STATE_NONE)
+        return;
+
+    g_clear_object(&priv->con_ip_config_x[IS_IPv4]);
+    g_clear_object(&priv->ext_ip_config_x[IS_IPv4]);
+    if (IS_IPv4) {
+        g_clear_object(&priv->dev_ip_config_4.current);
+    } else {
+        g_clear_object(&priv->ac_ip6_config.current);
+        g_clear_object(&priv->dhcp6.ip6_config.current);
+    }
+    g_clear_object(&priv->dev2_ip_config_x[IS_IPv4].current);
+
+    if (!IS_IPv4) {
+        if (priv->ipv6ll_handle && !IN6_IS_ADDR_UNSPECIFIED(&priv->ipv6ll_addr))
+            priv->ipv6ll_has = TRUE;
+    }
+
+    priv->con_ip_config_x[IS_IPv4] = nm_device_ip_config_new(self, addr_family);
+
+    if (IS_IPv4) {
+        nm_ip4_config_merge_setting(priv->con_ip_config_4,
+                                    s_ip_new,
+                                    _prop_get_connection_mdns(self),
+                                    _prop_get_connection_llmnr(self),
+                                    nm_device_get_route_table(self, AF_INET),
+                                    nm_device_get_route_metric(self, AF_INET));
+    } else {
+        nm_ip6_config_merge_setting(priv->con_ip_config_6,
+                                    s_ip_new,
+                                    nm_device_get_route_table(self, AF_INET6),
+                                    nm_device_get_route_metric(self, AF_INET6));
+    }
+
+    method_old = (s_ip_old ? nm_setting_ip_config_get_method(s_ip_old) : NULL)
+                     ?: (IS_IPv4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED
+                                 : NM_SETTING_IP6_CONFIG_METHOD_IGNORE);
+    method_new = (s_ip_new ? nm_setting_ip_config_get_method(s_ip_new) : NULL)
+                     ?: (IS_IPv4 ? NM_SETTING_IP4_CONFIG_METHOD_DISABLED
+                                 : NM_SETTING_IP6_CONFIG_METHOD_IGNORE);
+
+    if (!nm_streq0(method_old, method_new)) {
+        _cleanup_ip_pre(self, addr_family, CLEANUP_TYPE_DECONFIGURE);
+        _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_WAIT);
+        if (!nm_device_activate_stage3_ip_start(self, addr_family)) {
+            _LOGW(LOGD_IP4,
+                  "Failed to apply IPv%c configuration",
+                  nm_utils_addr_family_to_char(addr_family));
+        }
+        return;
+    }
+
+    if (s_ip_old && s_ip_new) {
+        gint64 metric_old, metric_new;
+
+        /* For dynamic IP methods (DHCP, IPv4LL, WWAN) the route metric is
+         * set at activation/renewal time using the value from static
+         * configuration. To support runtime change we need to update the
+         * dynamic configuration in place and tell the DHCP client the new
+         * value to use for future renewals.
+         */
+        metric_old = nm_setting_ip_config_get_route_metric(s_ip_old);
+        metric_new = nm_setting_ip_config_get_route_metric(s_ip_new);
+
+        if (metric_old != metric_new) {
+            if (IS_IPv4) {
+                if (priv->dev_ip_config_4.orig) {
+                    nm_ip4_config_update_routes_metric((NMIP4Config *) priv->dev_ip_config_4.orig,
+                                                       nm_device_get_route_metric(self, AF_INET));
+                }
+                if (priv->dev2_ip_config_4.orig) {
+                    nm_ip4_config_update_routes_metric((NMIP4Config *) priv->dev2_ip_config_4.orig,
+                                                       nm_device_get_route_metric(self, AF_INET));
+                }
+                if (priv->dhcp_data_4.client) {
+                    nm_dhcp_client_set_route_metric(priv->dhcp_data_4.client,
+                                                    nm_device_get_route_metric(self, AF_INET));
+                }
+            } else {
+                if (priv->ac_ip6_config.orig) {
+                    nm_ip6_config_update_routes_metric((NMIP6Config *) priv->ac_ip6_config.orig,
+                                                       nm_device_get_route_metric(self, AF_INET6));
+                }
+                if (priv->dhcp6.ip6_config.orig) {
+                    nm_ip6_config_update_routes_metric((NMIP6Config *) priv->dhcp6.ip6_config.orig,
+                                                       nm_device_get_route_metric(self, AF_INET6));
+                }
+                if (priv->dev2_ip_config_6.orig) {
+                    nm_ip6_config_update_routes_metric((NMIP6Config *) priv->dev2_ip_config_6.orig,
+                                                       nm_device_get_route_metric(self, AF_INET6));
+                }
+                if (priv->dhcp_data_6.client) {
+                    nm_dhcp_client_set_route_metric(priv->dhcp_data_6.client,
+                                                    nm_device_get_route_metric(self, AF_INET6));
+                }
+            }
+        }
+    }
+
+    if (nm_device_get_ip_ifindex(self) > 0 && !ip_config_merge_and_apply(self, addr_family, TRUE)) {
+        _LOGW(LOGD_IPX(IS_IPv4),
+              "Failed to reapply IPv%c configuration",
+              nm_utils_addr_family_to_char(addr_family));
+    }
+}
+
+static void
+_pacrunner_manager_add(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
+
+    priv->pacrunner_conf_id = nm_pacrunner_manager_add(nm_pacrunner_manager_get(),
+                                                       priv->proxy_config,
+                                                       nm_device_get_ip_iface(self),
+                                                       NULL,
+                                                       NULL);
+}
+
+static void
+reactivate_proxy_config(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->pacrunner_conf_id)
+        return;
+    nm_device_set_proxy_config(self, priv->dhcp4.pac_url);
+    _pacrunner_manager_add(self);
+}
+
+static gboolean
+can_reapply_change(NMDevice *  self,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    if (nm_streq(setting_name, NM_SETTING_CONNECTION_SETTING_NAME)) {
+        /* Whitelist allowed properties from "connection" setting which are
+         * allowed to differ.
+         *
+         * This includes UUID, there is no principal problem with reapplying a
+         * connection and changing its UUID. In fact, disallowing it makes it
+         * cumbersome for the user to reapply any connection but the original
+         * settings-connection. */
+        return nm_device_hash_check_invalid_keys(diffs,
+                                                 NM_SETTING_CONNECTION_SETTING_NAME,
+                                                 error,
+                                                 NM_SETTING_CONNECTION_ID,
+                                                 NM_SETTING_CONNECTION_UUID,
+                                                 NM_SETTING_CONNECTION_STABLE_ID,
+                                                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                                                 NM_SETTING_CONNECTION_ZONE,
+                                                 NM_SETTING_CONNECTION_METERED,
+                                                 NM_SETTING_CONNECTION_LLDP,
+                                                 NM_SETTING_CONNECTION_MDNS,
+                                                 NM_SETTING_CONNECTION_LLMNR);
+    }
+
+    if (NM_IN_STRSET(setting_name,
+                     NM_SETTING_USER_SETTING_NAME,
+                     NM_SETTING_PROXY_SETTING_NAME,
+                     NM_SETTING_IP4_CONFIG_SETTING_NAME,
+                     NM_SETTING_IP6_CONFIG_SETTING_NAME))
+        return TRUE;
+
+    if (nm_streq(setting_name, NM_SETTING_WIRED_SETTING_NAME)) {
+        if (NM_IN_SET(NM_DEVICE_GET_CLASS(self)->get_configured_mtu,
+                      nm_device_get_configured_mtu_wired_parent,
+                      nm_device_get_configured_mtu_for_wired)) {
+            return nm_device_hash_check_invalid_keys(diffs,
+                                                     NM_SETTING_WIRED_SETTING_NAME,
+                                                     error,
+                                                     NM_SETTING_WIRED_MTU);
+        }
+        goto out_fail;
+    }
+
+    if (nm_streq(setting_name, NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME)
+        && NM_DEVICE_GET_CLASS(self)->can_reapply_change_ovs_external_ids) {
+        /* TODO: this means, you cannot reapply changes to the external-ids for
+         * OVS system interfaces. */
+        return TRUE;
+    }
+
+out_fail:
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                "Can't reapply any changes to '%s' setting",
+                setting_name);
+    return FALSE;
+}
+
+static void
+reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new)
+{}
+
+/* check_and_reapply_connection:
+ * @connection: the new connection settings to be applied or %NULL to reapply
+ *   the current settings connection
+ * @version_id: either zero, or the current version id for the applied
+ *   connection.
+ * @audit_args: on return, a string representing the changes
+ * @error: the error if %FALSE is returned
+ *
+ * Change configuration of an already configured device if possible.
+ * Updates the device's applied connection upon success.
+ *
+ * Return: %FALSE if the new configuration can not be reapplied.
+ */
+static gboolean
+check_and_reapply_connection(NMDevice *    self,
+                             NMConnection *connection,
+                             guint64       version_id,
+                             char **       audit_args,
+                             GError **     error)
+{
+    NMDeviceClass *  klass                      = NM_DEVICE_GET_CLASS(self);
+    NMDevicePrivate *priv                       = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   applied                    = nm_device_get_applied_connection(self);
+    gs_unref_object NMConnection *applied_clone = NULL;
+    gs_unref_hashtable GHashTable *diffs        = NULL;
+    NMConnection *                 con_old, *con_new;
+    NMSettingIPConfig *            s_ip4_old, *s_ip4_new;
+    NMSettingIPConfig *            s_ip6_old, *s_ip6_new;
+    GHashTableIter                 iter;
+
+    if (priv->state < NM_DEVICE_STATE_PREPARE || priv->state > NM_DEVICE_STATE_ACTIVATED) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_NOT_ACTIVE,
+                            "Device is not activated");
+        return FALSE;
+    }
+
+    nm_connection_diff(connection,
+                       applied,
+                       NM_SETTING_COMPARE_FLAG_IGNORE_TIMESTAMP
+                           | NM_SETTING_COMPARE_FLAG_IGNORE_SECRETS,
+                       &diffs);
+
+    if (audit_args) {
+        if (diffs && nm_audit_manager_audit_enabled(nm_audit_manager_get()))
+            *audit_args = nm_utils_format_con_diff_for_audit(diffs);
+        else
+            *audit_args = NULL;
+    }
+
+    /**************************************************************************
+     * check for unsupported changes and reject to reapply
+     *************************************************************************/
+    if (diffs) {
+        char *      setting_name;
+        GHashTable *setting_diff;
+
+        g_hash_table_iter_init(&iter, diffs);
+        while (
+            g_hash_table_iter_next(&iter, (gpointer *) &setting_name, (gpointer *) &setting_diff)) {
+            if (!klass->can_reapply_change(
+                    self,
+                    setting_name,
+                    nm_connection_get_setting_by_name(applied, setting_name),
+                    nm_connection_get_setting_by_name(connection, setting_name),
+                    setting_diff,
+                    error))
+                return FALSE;
+        }
+    }
+
+    if (version_id != 0
+        && version_id
+               != nm_active_connection_version_id_get(
+                   (NMActiveConnection *) priv->act_request.obj)) {
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_VERSION_ID_MISMATCH,
+            "Reapply failed because device changed in the meantime and the version-id mismatches");
+        return FALSE;
+    }
+
+    /**************************************************************************
+     * Update applied connection
+     *************************************************************************/
+
+    if (diffs)
+        nm_active_connection_version_id_bump((NMActiveConnection *) priv->act_request.obj);
+
+    _LOGD(LOGD_DEVICE,
+          "reapply (version-id %llu%s)",
+          (unsigned long long) nm_active_connection_version_id_get(
+              ((NMActiveConnection *) priv->act_request.obj)),
+          diffs ? "" : " (unmodified)");
+
+    if (diffs) {
+        NMConnection *  connection_clean                    = connection;
+        gs_unref_object NMConnection *connection_clean_free = NULL;
+
+        {
+            NMSettingConnection *s_con_a, *s_con_n;
+
+            /* we allow re-applying a connection with differing ID, UUID, STABLE_ID and AUTOCONNECT.
+             * This is for convenience but these values are not actually changeable. So, check
+             * if they changed, and if the did revert to the original values. */
+            s_con_a = nm_connection_get_setting_connection(applied);
+            s_con_n = nm_connection_get_setting_connection(connection);
+
+            if (!nm_streq(nm_setting_connection_get_id(s_con_a),
+                          nm_setting_connection_get_id(s_con_n))
+                || !nm_streq(nm_setting_connection_get_uuid(s_con_a),
+                             nm_setting_connection_get_uuid(s_con_n))
+                || nm_setting_connection_get_autoconnect(s_con_a)
+                       != nm_setting_connection_get_autoconnect(s_con_n)
+                || !nm_streq0(nm_setting_connection_get_stable_id(s_con_a),
+                              nm_setting_connection_get_stable_id(s_con_n))) {
+                connection_clean_free = nm_simple_connection_new_clone(connection);
+                connection_clean      = connection_clean_free;
+                s_con_n               = nm_connection_get_setting_connection(connection_clean);
+                g_object_set(s_con_n,
+                             NM_SETTING_CONNECTION_ID,
+                             nm_setting_connection_get_id(s_con_a),
+                             NM_SETTING_CONNECTION_UUID,
+                             nm_setting_connection_get_uuid(s_con_a),
+                             NM_SETTING_CONNECTION_AUTOCONNECT,
+                             nm_setting_connection_get_autoconnect(s_con_a),
+                             NM_SETTING_CONNECTION_STABLE_ID,
+                             nm_setting_connection_get_stable_id(s_con_a),
+                             NULL);
+            }
+        }
+
+        con_old = applied_clone = nm_simple_connection_new_clone(applied);
+        con_new                 = applied;
+        /* FIXME(applied-connection-immutable): we should not modify the applied
+         *   connection but replace it with a new (immutable) instance. */
+        nm_connection_replace_settings_from_connection(applied, connection_clean);
+        nm_connection_clear_secrets(applied);
+    } else
+        con_old = con_new = applied;
+
+    priv->v4_commit_first_time = TRUE;
+    priv->v6_commit_first_time = TRUE;
+
+    priv->v4_route_table_initialized = FALSE;
+    priv->v6_route_table_initialized = FALSE;
+
+    /**************************************************************************
+     * Reapply changes
+     *
+     * Note that reapply_connection() is called as very first. This is for example
+     * important for NMDeviceWireGuard, which implements coerce_route_table()
+     * and get_extra_rules().
+     * That is because NMDeviceWireGuard caches settings, so during reapply that
+     * cache must be updated *first*.
+     *************************************************************************/
+    klass->reapply_connection(self, con_old, con_new);
+
+    if (priv->state >= NM_DEVICE_STATE_CONFIG)
+        lldp_init(self, FALSE);
+
+    if (priv->state >= NM_DEVICE_STATE_IP_CONFIG) {
+        s_ip4_old = nm_connection_get_setting_ip4_config(con_old);
+        s_ip4_new = nm_connection_get_setting_ip4_config(con_new);
+        s_ip6_old = nm_connection_get_setting_ip6_config(con_old);
+        s_ip6_new = nm_connection_get_setting_ip6_config(con_new);
+
+        /* Allow reapply of MTU */
+        priv->mtu_source = NM_DEVICE_MTU_SOURCE_NONE;
+
+        nm_device_reactivate_ip_config(self, AF_INET, s_ip4_old, s_ip4_new);
+        nm_device_reactivate_ip_config(self, AF_INET6, s_ip6_old, s_ip6_new);
+
+        _routing_rules_sync(self, NM_TERNARY_TRUE);
+
+        reactivate_proxy_config(self);
+    }
+
+    if (priv->state >= NM_DEVICE_STATE_IP_CHECK)
+        nm_device_update_firewall_zone(self);
+
+    if (priv->state >= NM_DEVICE_STATE_ACTIVATED)
+        nm_device_update_metered(self);
+
+    return TRUE;
+}
+
+gboolean
+nm_device_reapply(NMDevice *self, NMConnection *connection, GError **error)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    return check_and_reapply_connection(self, connection, 0, NULL, error);
+}
+
+typedef struct {
+    NMConnection *connection;
+    guint64       version_id;
+} ReapplyData;
+
+static void
+reapply_cb(NMDevice *             self,
+           GDBusMethodInvocation *context,
+           NMAuthSubject *        subject,
+           GError *               error,
+           gpointer               user_data)
+{
+    ReapplyData *   reapply_data             = user_data;
+    guint64         version_id               = 0;
+    gs_unref_object NMConnection *connection = NULL;
+    GError *                      local      = NULL;
+    gs_free char *                audit_args = NULL;
+
+    if (reapply_data) {
+        connection = reapply_data->connection;
+        version_id = reapply_data->version_id;
+        g_slice_free(ReapplyData, reapply_data);
+    }
+
+    if (error) {
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               error->message);
+        g_dbus_method_invocation_return_gerror(context, error);
+        return;
+    }
+
+    if (nm_device_sys_iface_state_is_external(self))
+        nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED);
+
+    if (!check_and_reapply_connection(self,
+                                      connection
+                                          ?: nm_device_get_settings_connection_get_connection(self),
+                                      version_id,
+                                      &audit_args,
+                                      &local)) {
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY,
+                               self,
+                               FALSE,
+                               audit_args,
+                               subject,
+                               local->message);
+        g_dbus_method_invocation_take_error(context, local);
+        local = NULL;
+    } else {
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY, self, TRUE, audit_args, subject, NULL);
+        g_dbus_method_invocation_return_value(context, NULL);
+    }
+}
+
+static void
+impl_device_reapply(NMDBusObject *                     obj,
+                    const NMDBusInterfaceInfoExtended *interface_info,
+                    const NMDBusMethodInfoExtended *   method_info,
+                    GDBusConnection *                  dbus_connection,
+                    const char *                       sender,
+                    GDBusMethodInvocation *            invocation,
+                    GVariant *                         parameters)
+{
+    NMDevice *            self = NM_DEVICE(obj);
+    NMDevicePrivate *     priv = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingsConnection *settings_connection;
+    NMConnection *        connection = NULL;
+    GError *              error      = NULL;
+    ReapplyData *         reapply_data;
+    gs_unref_variant GVariant *settings = NULL;
+    guint64                    version_id;
+    guint32                    flags;
+
+    g_variant_get(parameters, "(@a{sa{sv}}tu)", &settings, &version_id, &flags);
+
+    /* No flags supported as of now. */
+    if (flags != 0) {
+        error =
+            g_error_new_literal(NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "Invalid flags specified");
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY,
+                               self,
+                               FALSE,
+                               NULL,
+                               invocation,
+                               error->message);
+        g_dbus_method_invocation_take_error(invocation, error);
+        return;
+    }
+
+    if (priv->state < NM_DEVICE_STATE_PREPARE || priv->state > NM_DEVICE_STATE_ACTIVATED) {
+        error = g_error_new_literal(NM_DEVICE_ERROR,
+                                    NM_DEVICE_ERROR_NOT_ACTIVE,
+                                    "Device is not activated");
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY,
+                               self,
+                               FALSE,
+                               NULL,
+                               invocation,
+                               error->message);
+        g_dbus_method_invocation_take_error(invocation, error);
+        return;
+    }
+
+    settings_connection = nm_device_get_settings_connection(self);
+    g_return_if_fail(settings_connection);
+
+    if (settings && g_variant_n_children(settings)) {
+        /* New settings specified inline. */
+        connection = _nm_simple_connection_new_from_dbus(settings,
+                                                         NM_SETTING_PARSE_FLAGS_STRICT
+                                                             | NM_SETTING_PARSE_FLAGS_NORMALIZE,
+                                                         &error);
+        if (!connection) {
+            g_prefix_error(&error, "The settings specified are invalid: ");
+            nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_REAPPLY,
+                                   self,
+                                   FALSE,
+                                   NULL,
+                                   invocation,
+                                   error->message);
+            g_dbus_method_invocation_take_error(invocation, error);
+            return;
+        }
+        nm_connection_clear_secrets(connection);
+    }
+
+    if (connection || version_id) {
+        reapply_data             = g_slice_new(ReapplyData);
+        reapply_data->connection = connection;
+        reapply_data->version_id = version_id;
+    } else
+        reapply_data = NULL;
+
+    nm_device_auth_request(self,
+                           invocation,
+                           nm_device_get_applied_connection(self),
+                           NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                           TRUE,
+                           NULL,
+                           reapply_cb,
+                           reapply_data);
+}
+
+/*****************************************************************************/
+
+static void
+impl_device_get_applied_connection(NMDBusObject *                     obj,
+                                   const NMDBusInterfaceInfoExtended *interface_info,
+                                   const NMDBusMethodInfoExtended *   method_info,
+                                   GDBusConnection *                  connection,
+                                   const char *                       sender,
+                                   GDBusMethodInvocation *            invocation,
+                                   GVariant *                         parameters)
+{
+    NMDevice *       self       = NM_DEVICE(obj);
+    NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
+    NMConnection *        applied_connection;
+    guint32               flags;
+    GVariant *            var_settings;
+
+    g_variant_get(parameters, "(u)", &flags);
+
+    /* No flags supported as of now. */
+    if (flags != 0) {
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_FAILED,
+                                                      "Invalid flags specified");
+        return;
+    }
+
+    applied_connection = nm_device_get_applied_connection(self);
+    if (!applied_connection) {
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ACTIVE,
+                                                      "Device is not activated");
+        return;
+    }
+
+    if (!nm_auth_is_invocation_in_acl_set_error(applied_connection,
+                                                invocation,
+                                                NM_MANAGER_ERROR,
+                                                NM_MANAGER_ERROR_PERMISSION_DENIED,
+                                                NULL,
+                                                &error)) {
+        g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error));
+        return;
+    }
+
+    var_settings = nm_connection_to_dbus(applied_connection, NM_CONNECTION_SERIALIZE_NO_SECRETS);
+    if (!var_settings)
+        var_settings = g_variant_new_array(G_VARIANT_TYPE("{sa{sv}}"), NULL, 0);
+
+    g_dbus_method_invocation_return_value(
+        invocation,
+        g_variant_new(
+            "(@a{sa{sv}}t)",
+            var_settings,
+            nm_active_connection_version_id_get((NMActiveConnection *) priv->act_request.obj)));
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    gint64 timestamp_ms;
+    bool   dirty;
+} IP6RoutesTemporaryNotAvailableData;
+
+static gboolean
+_rt6_temporary_not_available_timeout(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->rt6_temporary_not_available_id = 0;
+    nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+_rt6_temporary_not_available_set(NMDevice *self, GPtrArray *temporary_not_available)
+{
+    NMDevicePrivate *                   priv = NM_DEVICE_GET_PRIVATE(self);
+    IP6RoutesTemporaryNotAvailableData *data;
+    GHashTableIter                      iter;
+    gint64                              now_ms, oldest_ms;
+    const gint64                        MAX_AGE_MS = 20000;
+    guint                               i;
+    gboolean                            success = TRUE;
+
+    if (!temporary_not_available || !temporary_not_available->len) {
+        /* nothing outstanding. Clear tracking the routes. */
+        nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref);
+        nm_clear_g_source(&priv->rt6_temporary_not_available_id);
+        return success;
+    }
+
+    if (priv->rt6_temporary_not_available) {
+        g_hash_table_iter_init(&iter, priv->rt6_temporary_not_available);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &data))
+            data->dirty = TRUE;
+    } else {
+        priv->rt6_temporary_not_available =
+            g_hash_table_new_full((GHashFunc) nmp_object_id_hash,
+                                  (GEqualFunc) nmp_object_id_equal,
+                                  (GDestroyNotify) nmp_object_unref,
+                                  nm_g_slice_free_fcn(IP6RoutesTemporaryNotAvailableData));
+    }
+
+    now_ms    = nm_utils_get_monotonic_timestamp_msec();
+    oldest_ms = now_ms;
+
+    for (i = 0; i < temporary_not_available->len; i++) {
+        const NMPObject *o = temporary_not_available->pdata[i];
+
+        data = g_hash_table_lookup(priv->rt6_temporary_not_available, o);
+        if (data) {
+            if (!data->dirty)
+                continue;
+            data->dirty = FALSE;
+            nm_assert(data->timestamp_ms > 0 && data->timestamp_ms <= now_ms);
+            if (now_ms > data->timestamp_ms + MAX_AGE_MS) {
+                /* timeout. Could not add this address. */
+                _LOGW(LOGD_DEVICE,
+                      "failure to add IPv6 route: %s",
+                      nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, NULL, 0));
+                success = FALSE;
+            } else
+                oldest_ms = MIN(data->timestamp_ms, oldest_ms);
+            continue;
+        }
+
+        data               = g_slice_new0(IP6RoutesTemporaryNotAvailableData);
+        data->timestamp_ms = now_ms;
+        g_hash_table_insert(priv->rt6_temporary_not_available, (gpointer) nmp_object_ref(o), data);
+    }
+
+    g_hash_table_iter_init(&iter, priv->rt6_temporary_not_available);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &data)) {
+        if (data->dirty)
+            g_hash_table_iter_remove(&iter);
+    }
+
+    nm_clear_g_source(&priv->rt6_temporary_not_available_id);
+    priv->rt6_temporary_not_available_id =
+        g_timeout_add(oldest_ms + MAX_AGE_MS - now_ms, _rt6_temporary_not_available_timeout, self);
+
+    return success;
+}
+
+/*****************************************************************************/
+
+static void
+disconnect_cb(NMDevice *             self,
+              GDBusMethodInvocation *context,
+              NMAuthSubject *        subject,
+              GError *               error,
+              gpointer               user_data)
+{
+    NMDevicePrivate *priv  = NM_DEVICE_GET_PRIVATE(self);
+    GError *         local = NULL;
+
+    if (error) {
+        g_dbus_method_invocation_return_gerror(context, error);
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               error->message);
+        return;
+    }
+
+    /* Authorized */
+    if (priv->state <= NM_DEVICE_STATE_DISCONNECTED) {
+        local = g_error_new_literal(NM_DEVICE_ERROR,
+                                    NM_DEVICE_ERROR_NOT_ACTIVE,
+                                    "Device is not active");
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               local->message);
+        g_dbus_method_invocation_take_error(context, local);
+    } else {
+        nm_device_autoconnect_blocked_set(self, NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT);
+
+        nm_device_state_changed(self,
+                                NM_DEVICE_STATE_DEACTIVATING,
+                                NM_DEVICE_STATE_REASON_USER_REQUESTED);
+        g_dbus_method_invocation_return_value(context, NULL);
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DISCONNECT, self, TRUE, NULL, subject, NULL);
+    }
+}
+
+static void
+_clear_queued_act_request(NMDevicePrivate *priv, NMActiveConnectionStateReason active_reason)
+{
+    if (priv->queued_act_request) {
+        gs_unref_object NMActRequest *ac = NULL;
+
+        ac = g_steal_pointer(&priv->queued_act_request);
+        nm_active_connection_set_state_fail((NMActiveConnection *) ac, active_reason, NULL);
+    }
+}
+
+static void
+impl_device_disconnect(NMDBusObject *                     obj,
+                       const NMDBusInterfaceInfoExtended *interface_info,
+                       const NMDBusMethodInfoExtended *   method_info,
+                       GDBusConnection *                  dbus_connection,
+                       const char *                       sender,
+                       GDBusMethodInvocation *            invocation,
+                       GVariant *                         parameters)
+{
+    NMDevice *       self = NM_DEVICE(obj);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+
+    if (!priv->act_request.obj) {
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ACTIVE,
+                                                      "This device is not active");
+        return;
+    }
+
+    connection = nm_device_get_applied_connection(self);
+    nm_assert(connection);
+
+    nm_device_auth_request(self,
+                           invocation,
+                           connection,
+                           NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                           TRUE,
+                           NULL,
+                           disconnect_cb,
+                           NULL);
+}
+
+static void
+delete_cb(NMDevice *             self,
+          GDBusMethodInvocation *context,
+          NMAuthSubject *        subject,
+          GError *               error,
+          gpointer               user_data)
+{
+    GError *local = NULL;
+
+    if (error) {
+        g_dbus_method_invocation_return_gerror(context, error);
+        nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DELETE,
+                               self,
+                               FALSE,
+                               NULL,
+                               subject,
+                               error->message);
+        return;
+    }
+
+    /* Authorized */
+    nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DELETE, self, TRUE, NULL, subject, NULL);
+    if (nm_device_unrealize(self, TRUE, &local))
+        g_dbus_method_invocation_return_value(context, NULL);
+    else
+        g_dbus_method_invocation_take_error(context, local);
+}
+
+static void
+impl_device_delete(NMDBusObject *                     obj,
+                   const NMDBusInterfaceInfoExtended *interface_info,
+                   const NMDBusMethodInfoExtended *   method_info,
+                   GDBusConnection *                  connection,
+                   const char *                       sender,
+                   GDBusMethodInvocation *            invocation,
+                   GVariant *                         parameters)
+{
+    NMDevice *self = NM_DEVICE(obj);
+
+    if (!nm_device_is_software(self) || !nm_device_is_real(self)) {
+        g_dbus_method_invocation_return_error_literal(
+            invocation,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_NOT_SOFTWARE,
+            "This device is not a software device or is not realized");
+        return;
+    }
+
+    nm_device_auth_request(self,
+                           invocation,
+                           NULL,
+                           NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                           TRUE,
+                           NULL,
+                           delete_cb,
+                           NULL);
+}
+
+static void
+_device_activate(NMDevice *self, NMActRequest *req)
+{
+    NMConnection *connection;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+    nm_assert(nm_device_is_real(self));
+
+    /* Ensure the activation request is still valid; the master may have
+     * already failed in which case activation of this device should not proceed.
+     */
+    if (nm_active_connection_get_state(NM_ACTIVE_CONNECTION(req))
+        >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING)
+        return;
+
+    if (!nm_device_get_managed(self, FALSE)) {
+        /* It's unclear why the device would be unmanaged at this point.
+         * Just to be sure, handle it and error out. */
+        _LOGE(LOGD_DEVICE,
+              "Activation: failed activating connection '%s' because device is still unmanaged",
+              nm_active_connection_get_settings_connection_id((NMActiveConnection *) req));
+        nm_active_connection_set_state_fail((NMActiveConnection *) req,
+                                            NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN,
+                                            NULL);
+        return;
+    }
+
+    connection = nm_act_request_get_applied_connection(req);
+    nm_assert(connection);
+
+    _LOGI(LOGD_DEVICE,
+          "Activation: starting connection '%s' (%s)",
+          nm_connection_get_id(connection),
+          nm_connection_get_uuid(connection));
+
+    delete_on_deactivate_unschedule(self);
+
+    act_request_set(self, req);
+
+    nm_device_activate_schedule_stage1_device_prepare(self, FALSE);
+}
+
+static void
+_carrier_wait_check_queued_act_request(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->queued_act_request || !priv->queued_act_request_is_waiting_for_carrier)
+        return;
+
+    priv->queued_act_request_is_waiting_for_carrier = FALSE;
+    if (!priv->carrier) {
+        _LOGD(LOGD_DEVICE, "Cancel queued activation request as we have no carrier after timeout");
+        _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED);
+    } else if (priv->state == NM_DEVICE_STATE_DISCONNECTED) {
+        gs_unref_object NMActRequest *queued_req = NULL;
+
+        _LOGD(LOGD_DEVICE, "Activate queued activation request as we now have carrier");
+        queued_req = g_steal_pointer(&priv->queued_act_request);
+        _device_activate(self, queued_req);
+    }
+}
+
+static gboolean
+_carrier_wait_check_act_request_must_queue(NMDevice *self, NMActRequest *req)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *   connection;
+
+    /* If we have carrier or if we are not waiting for it, the activation
+     * request is not blocked waiting for carrier. */
+    if (priv->carrier)
+        return FALSE;
+    if (priv->carrier_wait_id == 0)
+        return FALSE;
+
+    connection = nm_act_request_get_applied_connection(req);
+    if (!connection_requires_carrier(connection))
+        return FALSE;
+
+    if (!nm_device_check_connection_available(self,
+                                              connection,
+                                              NM_DEVICE_CHECK_CON_AVAILABLE_ALL,
+                                              NULL,
+                                              NULL)) {
+        /* We passed all @flags we have, and no @specific_object.
+         * This equals maximal availability, if a connection is not available
+         * in this case, it is not waiting for carrier.
+         *
+         * Actually, why are we even trying to activate it? Strange, but whatever
+         * the reason, don't wait for carrier.
+         */
+        return FALSE;
+    }
+
+    if (nm_device_check_connection_available(
+            self,
+            connection,
+            NM_DEVICE_CHECK_CON_AVAILABLE_ALL
+                & ~_NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER,
+            NULL,
+            NULL)) {
+        /* The connection was available with flags ALL, and it is still available
+         * if we pretend not to wait for carrier. That means that the
+         * connection is available now, and does not wait for carrier.
+         *
+         * Since the flags increase the availability of a connection, when checking
+         * ALL&~WAITING_CARRIER, it means that we certainly would wait for carrier. */
+        return FALSE;
+    }
+
+    /* The activation request must wait for carrier. */
+    return TRUE;
+}
+
+void
+nm_device_disconnect_active_connection(NMActiveConnection *          active,
+                                       NMDeviceStateReason           device_reason,
+                                       NMActiveConnectionStateReason active_reason)
+{
+    NMDevice *       self;
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_ACTIVE_CONNECTION(active));
+
+    self = nm_active_connection_get_device(active);
+    if (!self) {
+        /* hm, no device? Just fail the active connection. */
+        goto do_fail;
+    }
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (NM_ACTIVE_CONNECTION(priv->queued_act_request) == active) {
+        _clear_queued_act_request(priv, active_reason);
+        return;
+    }
+
+    if (NM_ACTIVE_CONNECTION(priv->act_request.obj) == active) {
+        if (priv->state < NM_DEVICE_STATE_DEACTIVATING) {
+            /* When the user actively deactivates a profile, we set
+             * the sys-iface-state to managed so that we deconfigure/cleanup the interface.
+             * But for external connections that go down otherwise, we don't want to touch the interface. */
+            if (nm_device_sys_iface_state_is_external(self))
+                nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED);
+
+            nm_device_state_changed(self, NM_DEVICE_STATE_DEACTIVATING, device_reason);
+        } else {
+            /* @active is the current ac of @self, but it's going down already.
+             * Nothing to do. */
+        }
+        return;
+    }
+
+    /* the active connection references this device, but it's neither the
+     * queued_act_request nor the current act_request. Just set it to fail... */
+do_fail:
+    nm_active_connection_set_state_fail(active, active_reason, NULL);
+}
+
+void
+nm_device_queue_activation(NMDevice *self, NMActRequest *req)
+{
+    NMDevicePrivate *priv;
+    gboolean         must_queue;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    nm_keep_alive_arm(nm_active_connection_get_keep_alive(NM_ACTIVE_CONNECTION(req)));
+
+    if (nm_active_connection_get_state(NM_ACTIVE_CONNECTION(req))
+        >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
+        /* it's already deactivating. Nothing to do. */
+        nm_assert(
+            NM_IN_SET(nm_active_connection_get_device(NM_ACTIVE_CONNECTION(req)), NULL, self));
+        return;
+    }
+
+    nm_assert(self == nm_active_connection_get_device(NM_ACTIVE_CONNECTION(req)));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    must_queue = _carrier_wait_check_act_request_must_queue(self, req);
+
+    if (!priv->act_request.obj && !must_queue && nm_device_is_real(self)) {
+        _device_activate(self, req);
+        return;
+    }
+
+    /* supersede any already-queued request */
+    _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED);
+    priv->queued_act_request                        = g_object_ref(req);
+    priv->queued_act_request_is_waiting_for_carrier = must_queue;
+
+    _LOGD(LOGD_DEVICE,
+          "queue activation request waiting for %s",
+          must_queue ? "carrier" : "currently active connection to disconnect");
+
+    /* Deactivate existing activation request first */
+    if (priv->act_request.obj) {
+        _LOGI(LOGD_DEVICE, "disconnecting for new activation request.");
+        nm_device_state_changed(self,
+                                NM_DEVICE_STATE_DEACTIVATING,
+                                NM_DEVICE_STATE_REASON_NEW_ACTIVATION);
+    }
+}
+
+/*
+ * nm_device_is_activating
+ *
+ * Return whether or not the device is currently activating itself.
+ *
+ */
+gboolean
+nm_device_is_activating(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceState    state;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    state = nm_device_get_state(self);
+    if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_SECONDARIES)
+        return TRUE;
+
+    /* There's a small race between the time when stage 1 is scheduled
+     * and when the device actually sets STATE_PREPARE when the activation
+     * handler is actually run.  If there's an activation handler scheduled
+     * we're activating anyway.
+     */
+    return priv->activation_source_id_4 != 0;
+}
+
+NMProxyConfig *
+nm_device_get_proxy_config(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->proxy_config;
+}
+
+static void
+nm_device_set_proxy_config(NMDevice *self, const char *pac_url)
+{
+    NMDevicePrivate *priv;
+    NMConnection *   connection;
+    NMSettingProxy * s_proxy = NULL;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_clear_object(&priv->proxy_config);
+    priv->proxy_config = nm_proxy_config_new();
+
+    if (pac_url) {
+        nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_AUTO);
+        nm_proxy_config_set_pac_url(priv->proxy_config, pac_url);
+        _LOGD(LOGD_PROXY, "proxy: PAC url \"%s\"", pac_url);
+    } else
+        nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_NONE);
+
+    connection = nm_device_get_applied_connection(self);
+    if (connection)
+        s_proxy = nm_connection_get_setting_proxy(connection);
+
+    if (s_proxy)
+        nm_proxy_config_merge_setting(priv->proxy_config, s_proxy);
+}
+
+/* IP Configuration stuff */
+NMDhcpConfig *
+nm_device_get_dhcp_config(NMDevice *self, int addr_family)
+{
+    const int IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    nm_assert_addr_family(addr_family);
+
+    return NM_DEVICE_GET_PRIVATE(self)->dhcp_data_x[IS_IPv4].config;
+}
+
+NMIP4Config *
+nm_device_get_ip4_config(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->ip_config_4;
+}
+
+static gboolean
+nm_device_set_ip_config(NMDevice *  self,
+                        int         addr_family,
+                        NMIPConfig *new_config,
+                        gboolean    commit,
+                        GPtrArray * ip4_dev_route_blacklist)
+{
+    NMDevicePrivate *     priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int             IS_IPv4 = NM_IS_IPv4(addr_family);
+    NMIPConfig *          old_config;
+    gboolean              has_changes = FALSE;
+    gboolean              success     = TRUE;
+    NMSettingsConnection *settings_connection;
+
+    nm_assert_addr_family(addr_family);
+    nm_assert(!new_config || nm_ip_config_get_addr_family(new_config) == addr_family);
+    nm_assert(!new_config
+              || (new_config && ({
+                      int ip_ifindex = nm_device_get_ip_ifindex(self);
+
+                      (ip_ifindex > 0 && ip_ifindex == nm_ip_config_get_ifindex(new_config));
+                  })));
+    nm_assert(IS_IPv4 || !ip4_dev_route_blacklist);
+
+    _LOGD(LOGD_IPX(IS_IPv4),
+          "ip%c-config: update (commit=%d, new-config=%p)",
+          nm_utils_addr_family_to_char(addr_family),
+          commit,
+          new_config);
+
+    /* Always commit to nm-platform to update lifetimes */
+    if (commit && new_config) {
+        _commit_mtu(self, IS_IPv4 ? NM_IP4_CONFIG(new_config) : priv->ip_config_4);
+
+        if (IS_IPv4) {
+            success = nm_ip4_config_commit(NM_IP4_CONFIG(new_config),
+                                           nm_device_get_platform(self),
+                                           _get_route_table_sync_mode_stateful(self, AF_INET));
+            nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self),
+                                                    nm_ip_config_get_ifindex(new_config),
+                                                    ip4_dev_route_blacklist);
+        } else {
+            gs_unref_ptrarray GPtrArray *temporary_not_available = NULL;
+
+            success = nm_ip6_config_commit(NM_IP6_CONFIG(new_config),
+                                           nm_device_get_platform(self),
+                                           _get_route_table_sync_mode_stateful(self, AF_INET6),
+                                           &temporary_not_available);
+
+            if (!_rt6_temporary_not_available_set(self, temporary_not_available))
+                success = FALSE;
+        }
+    }
+
+    old_config = priv->ip_config_x[IS_IPv4];
+
+    if (new_config && old_config) {
+        /* has_changes is set only on relevant changes, because when the configuration changes,
+         * this causes a re-read and reset. This should only happen for relevant changes */
+        nm_ip_config_replace(old_config, new_config, &has_changes);
+        if (has_changes) {
+            _LOGD(LOGD_IPX(IS_IPv4),
+                  "ip%c-config: update IP Config instance (%s)",
+                  nm_utils_addr_family_to_char(addr_family),
+                  nm_dbus_object_get_path(NM_DBUS_OBJECT(old_config)));
+        }
+    } else if (new_config /*&& !old_config*/) {
+        has_changes                = TRUE;
+        priv->ip_config_x[IS_IPv4] = g_object_ref(new_config);
+        if (!nm_dbus_object_is_exported(NM_DBUS_OBJECT(new_config)))
+            nm_dbus_object_export(NM_DBUS_OBJECT(new_config));
+
+        _LOGD(LOGD_IPX(IS_IPv4),
+              "ip%c-config: set IP Config instance (%s)",
+              nm_utils_addr_family_to_char(addr_family),
+              nm_dbus_object_get_path(NM_DBUS_OBJECT(new_config)));
+    } else if (old_config /*&& !new_config*/) {
+        has_changes                = TRUE;
+        priv->ip_config_x[IS_IPv4] = NULL;
+        _LOGD(LOGD_IPX(IS_IPv4),
+              "ip%c-config: clear IP Config instance (%s)",
+              nm_utils_addr_family_to_char(addr_family),
+              nm_dbus_object_get_path(NM_DBUS_OBJECT(old_config)));
+        if (IS_IPv4) {
+            /* Device config is invalid if combined config is invalid */
+            applied_config_clear(&priv->dev_ip_config_4);
+        } else
+            priv->needs_ip6_subnet = FALSE;
+    }
+
+    if (has_changes) {
+        if (old_config != priv->ip_config_x[IS_IPv4])
+            _notify(self, IS_IPv4 ? PROP_IP4_CONFIG : PROP_IP6_CONFIG);
+
+        g_signal_emit(self,
+                      signals[IS_IPv4 ? IP4_CONFIG_CHANGED : IP6_CONFIG_CHANGED],
+                      0,
+                      priv->ip_config_x[IS_IPv4],
+                      old_config);
+
+        if (old_config != priv->ip_config_x[IS_IPv4])
+            nm_dbus_object_clear_and_unexport(&old_config);
+
+        if (nm_device_sys_iface_state_is_external(self)
+            && (settings_connection = nm_device_get_settings_connection(self))
+            && NM_FLAGS_HAS(nm_settings_connection_get_flags(settings_connection),
+                            NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL)
+            && nm_active_connection_get_activation_type(NM_ACTIVE_CONNECTION(priv->act_request.obj))
+                   == NM_ACTIVATION_TYPE_EXTERNAL) {
+            gs_unref_object NMConnection *new_connection = NULL;
+
+            new_connection = nm_simple_connection_new_clone(
+                nm_settings_connection_get_connection(settings_connection));
+
+            nm_connection_add_setting(
+                new_connection,
+                IS_IPv4 ? nm_ip4_config_create_setting(priv->ip_config_4)
+                        : nm_ip6_config_create_setting(priv->ip_config_6,
+                                                       _get_maybe_ipv6_disabled(self)));
+
+            nm_settings_connection_update(settings_connection,
+                                          new_connection,
+                                          NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY,
+                                          NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                          NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                          NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE,
+                                          "update-external",
+                                          NULL);
+        }
+
+        nm_device_queue_recheck_assume(self);
+
+        if (!IS_IPv4) {
+            if (priv->ndisc)
+                ndisc_set_router_config(priv->ndisc, self);
+        }
+    }
+
+    nm_assert(!old_config || old_config == priv->ip_config_x[IS_IPv4]);
+
+    return success;
+}
+
+static gboolean
+_replace_vpn_config_in_list(GSList **plist, GObject *old, GObject *new)
+{
+    GSList *old_link;
+
+    /* Below, assert that @new is not yet tracked, but still behave
+     * correctly in any case. Don't complain for missing @old since
+     * it could have been removed when the parent device became
+     * unmanaged. */
+
+    if (old && (old_link = g_slist_find(*plist, old))) {
+        if (old != new) {
+            if (new)
+                old_link->data = g_object_ref(new);
+            else
+                *plist = g_slist_delete_link(*plist, old_link);
+            g_object_unref(old);
+        }
+        return TRUE;
+    }
+
+    if (new) {
+        if (!g_slist_find(*plist, new))
+            *plist = g_slist_append(*plist, g_object_ref(new));
+        else
+            g_return_val_if_reached(TRUE);
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+void
+nm_device_replace_vpn4_config(NMDevice *self, NMIP4Config *old, NMIP4Config *config)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(!old || NM_IS_IP4_CONFIG(old));
+    nm_assert(!config || NM_IS_IP4_CONFIG(config));
+    nm_assert(!old || nm_ip4_config_get_ifindex(old) == nm_device_get_ip_ifindex(self));
+    nm_assert(!config || nm_ip4_config_get_ifindex(config) == nm_device_get_ip_ifindex(self));
+
+    if (!_replace_vpn_config_in_list(&priv->vpn_configs_4, (GObject *) old, (GObject *) config))
+        return;
+
+    /* NULL to use existing configs */
+    if (!ip_config_merge_and_apply(self, AF_INET, TRUE))
+        _LOGW(LOGD_IP4, "failed to set VPN routes for device");
+}
+
+void
+nm_device_set_dev2_ip_config(NMDevice *self, int addr_family, NMIPConfig *config)
+{
+    NMDevicePrivate *priv;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+    g_return_if_fail(!config || nm_ip_config_get_addr_family(config) == addr_family);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    applied_config_init(&priv->dev2_ip_config_x[IS_IPv4], config);
+    if (!ip_config_merge_and_apply(self, addr_family, TRUE)) {
+        _LOGW(LOGD_IP,
+              "failed to set extra device IPv%c configuration",
+              nm_utils_addr_family_to_char(addr_family));
+    }
+}
+
+void
+nm_device_replace_vpn6_config(NMDevice *self, NMIP6Config *old, NMIP6Config *config)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(!old || NM_IS_IP6_CONFIG(old));
+    nm_assert(!config || NM_IS_IP6_CONFIG(config));
+    nm_assert(!old || nm_ip6_config_get_ifindex(old) == nm_device_get_ip_ifindex(self));
+    nm_assert(!config || nm_ip6_config_get_ifindex(config) == nm_device_get_ip_ifindex(self));
+
+    if (!_replace_vpn_config_in_list(&priv->vpn_configs_6, (GObject *) old, (GObject *) config))
+        return;
+
+    /* NULL to use existing configs */
+    if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+        _LOGW(LOGD_IP6, "failed to set VPN routes for device");
+}
+
+NMIP6Config *
+nm_device_get_ip6_config(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->ip_config_6;
+}
+
+/*****************************************************************************/
+
+static gboolean
+dispatcher_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->dispatcher.call_id)
+        return FALSE;
+
+    nm_dispatcher_call_cancel(g_steal_pointer(&priv->dispatcher.call_id));
+    priv->dispatcher.post_state        = NM_DEVICE_STATE_UNKNOWN;
+    priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
+    return TRUE;
+}
+
+static void
+dispatcher_complete_proceed_state(NMDispatcherCallId *call_id, gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_if_fail(call_id == priv->dispatcher.call_id);
+
+    priv->dispatcher.call_id = NULL;
+    nm_device_queue_state(self, priv->dispatcher.post_state, priv->dispatcher.post_state_reason);
+    priv->dispatcher.post_state        = NM_DEVICE_STATE_UNKNOWN;
+    priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
+}
+
+/*****************************************************************************/
+
+static void
+ip_check_pre_up(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (dispatcher_cleanup(self))
+        nm_assert_not_reached();
+
+    priv->dispatcher.post_state        = NM_DEVICE_STATE_SECONDARIES;
+    priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
+    if (!nm_dispatcher_call_device(NM_DISPATCHER_ACTION_PRE_UP,
+                                   self,
+                                   NULL,
+                                   dispatcher_complete_proceed_state,
+                                   self,
+                                   &priv->dispatcher.call_id)) {
+        /* Just proceed on errors */
+        dispatcher_complete_proceed_state(0, self);
+    }
+}
+
+static void
+ip_check_gw_ping_cleanup(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->gw_ping.watch);
+    nm_clear_g_source(&priv->gw_ping.timeout);
+
+    if (priv->gw_ping.pid) {
+        nm_utils_kill_child_async(priv->gw_ping.pid,
+                                  SIGTERM,
+                                  priv->gw_ping.log_domain,
+                                  "ping",
+                                  1000,
+                                  NULL,
+                                  NULL);
+        priv->gw_ping.pid = 0;
+    }
+
+    nm_clear_g_free(&priv->gw_ping.binary);
+    nm_clear_g_free(&priv->gw_ping.address);
+}
+
+static gboolean
+spawn_ping(NMDevice *self)
+{
+    NMDevicePrivate *priv        = NM_DEVICE_GET_PRIVATE(self);
+    gs_free char *   str_timeout = NULL;
+    gs_free char *   tmp_str     = NULL;
+    const char *     args[]      = {priv->gw_ping.binary,
+                          "-I",
+                          nm_device_get_ip_iface(self),
+                          "-c",
+                          "1",
+                          "-w",
+                          NULL,
+                          priv->gw_ping.address,
+                          NULL};
+    gs_free_error GError *error  = NULL;
+    gboolean              ret;
+
+    args[6] = str_timeout = g_strdup_printf("%u", priv->gw_ping.deadline);
+    tmp_str               = g_strjoinv(" ", (char **) args);
+    _LOGD(priv->gw_ping.log_domain, "ping: running '%s'", tmp_str);
+
+    ret = g_spawn_async("/",
+                        (char **) args,
+                        NULL,
+                        G_SPAWN_DO_NOT_REAP_CHILD,
+                        NULL,
+                        NULL,
+                        &priv->gw_ping.pid,
+                        &error);
+
+    if (!ret) {
+        _LOGW(priv->gw_ping.log_domain,
+              "ping: could not spawn %s: %s",
+              priv->gw_ping.binary,
+              error->message);
+    }
+
+    return ret;
+}
+
+static gboolean
+respawn_ping_cb(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->gw_ping.watch = 0;
+
+    if (spawn_ping(self)) {
+        priv->gw_ping.watch = g_child_watch_add(priv->gw_ping.pid, ip_check_ping_watch_cb, self);
+    } else {
+        ip_check_gw_ping_cleanup(self);
+        ip_check_pre_up(self);
+    }
+
+    return FALSE;
+}
+
+static void
+ip_check_ping_watch_cb(GPid pid, int status, gpointer user_data)
+{
+    NMDevice *       self       = NM_DEVICE(user_data);
+    NMDevicePrivate *priv       = NM_DEVICE_GET_PRIVATE(self);
+    NMLogDomain      log_domain = priv->gw_ping.log_domain;
+    gboolean         success    = FALSE;
+
+    if (!priv->gw_ping.watch)
+        return;
+    priv->gw_ping.watch = 0;
+    priv->gw_ping.pid   = 0;
+
+    if (WIFEXITED(status)) {
+        if (WEXITSTATUS(status) == 0) {
+            _LOGD(log_domain, "ping: gateway ping succeeded");
+            success = TRUE;
+        } else {
+            _LOGW(log_domain, "ping: gateway ping failed with error code %d", WEXITSTATUS(status));
+        }
+    } else
+        _LOGW(log_domain, "ping: stopped unexpectedly with status %d", status);
+
+    if (success) {
+        /* We've got connectivity, proceed to pre_up */
+        ip_check_gw_ping_cleanup(self);
+        ip_check_pre_up(self);
+    } else {
+        /* If ping exited with an error it may have returned early,
+         * wait 1 second and restart it */
+        priv->gw_ping.watch = g_timeout_add_seconds(1, respawn_ping_cb, self);
+    }
+}
+
+static gboolean
+ip_check_ping_timeout_cb(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->gw_ping.timeout = 0;
+
+    _LOGW(priv->gw_ping.log_domain, "ping: gateway ping timed out");
+
+    ip_check_gw_ping_cleanup(self);
+    ip_check_pre_up(self);
+    return FALSE;
+}
+
+static gboolean
+start_ping(NMDevice *  self,
+           NMLogDomain log_domain,
+           const char *binary,
+           const char *address,
+           guint       timeout)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->gw_ping.watch == 0, FALSE);
+    g_return_val_if_fail(priv->gw_ping.timeout == 0, FALSE);
+
+    priv->gw_ping.log_domain = log_domain;
+    priv->gw_ping.address    = g_strdup(address);
+    priv->gw_ping.binary     = g_strdup(binary);
+    priv->gw_ping.deadline   = timeout + 10; /* the proper termination is enforced by a timer */
+
+    if (spawn_ping(self)) {
+        priv->gw_ping.watch   = g_child_watch_add(priv->gw_ping.pid, ip_check_ping_watch_cb, self);
+        priv->gw_ping.timeout = g_timeout_add_seconds(timeout, ip_check_ping_timeout_cb, self);
+        return TRUE;
+    }
+
+    ip_check_gw_ping_cleanup(self);
+    return FALSE;
+}
+
+static void
+nm_device_start_ip_check(NMDevice *self)
+{
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMConnection *       connection;
+    NMSettingConnection *s_con;
+    guint                timeout     = 0;
+    const char *         ping_binary = NULL;
+    char                 buf[NM_UTILS_INET_ADDRSTRLEN];
+    NMLogDomain          log_domain = LOGD_IP4;
+
+    /* Shouldn't be any active ping here, since IP_CHECK happens after the
+     * first IP method completes.  Any subsequently completing IP method doesn't
+     * get checked.
+     */
+    g_return_if_fail(!priv->gw_ping.watch);
+    g_return_if_fail(!priv->gw_ping.timeout);
+    g_return_if_fail(!priv->gw_ping.pid);
+    g_return_if_fail(priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE
+                     || priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE);
+
+    connection = nm_device_get_applied_connection(self);
+    g_assert(connection);
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    timeout = nm_setting_connection_get_gateway_ping_timeout(s_con);
+
+    buf[0] = '\0';
+    if (timeout) {
+        const NMPObject *gw;
+
+        if (priv->ip_config_4 && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) {
+            gw = nm_ip4_config_best_default_route_get(priv->ip_config_4);
+            if (gw) {
+                _nm_utils_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf);
+                ping_binary = nm_utils_find_helper("ping", "/usr/bin/ping", NULL);
+                log_domain  = LOGD_IP4;
+            }
+        } else if (priv->ip_config_6 && priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) {
+            gw = nm_ip6_config_best_default_route_get(priv->ip_config_6);
+            if (gw) {
+                _nm_utils_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf);
+                ping_binary = nm_utils_find_helper("ping6", "/usr/bin/ping6", NULL);
+                log_domain  = LOGD_IP6;
+            }
+        }
+    }
+
+    if (buf[0])
+        start_ping(self, log_domain, ping_binary, buf, timeout);
+
+    /* If no ping was started, just advance to pre_up */
+    if (!priv->gw_ping.pid)
+        ip_check_pre_up(self);
+}
+
+/*****************************************************************************/
+
+static gboolean
+carrier_wait_timeout(gpointer user_data)
+{
+    NMDevice *       self = NM_DEVICE(user_data);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->carrier_wait_id = 0;
+    nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+    if (!priv->carrier)
+        _carrier_wait_check_queued_act_request(self);
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+nm_device_is_up(NMDevice *self)
+{
+    int ifindex;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    return ifindex > 0 ? nm_platform_link_is_up(nm_device_get_platform(self), ifindex) : TRUE;
+}
+
+static gint64
+_get_carrier_wait_ms(NMDevice *self)
+{
+    gs_free char *value = NULL;
+
+    value = nm_config_data_get_device_config(NM_CONFIG_GET_DATA,
+                                             NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT,
+                                             self,
+                                             NULL);
+    return _nm_utils_ascii_str_to_int64(value, 10, 0, G_MAXINT32, CARRIER_WAIT_TIME_MS);
+}
+
+gboolean
+nm_device_bring_up(NMDevice *self, gboolean block, gboolean *no_firmware)
+{
+    NMDevicePrivate *    priv         = NM_DEVICE_GET_PRIVATE(self);
+    gboolean             device_is_up = FALSE;
+    NMDeviceCapabilities capabilities;
+    int                  ifindex;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    NM_SET_OUT(no_firmware, FALSE);
+
+    if (!nm_device_get_enabled(self)) {
+        _LOGD(LOGD_PLATFORM, "bringing up device ignored due to disabled");
+        return FALSE;
+    }
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    _LOGD(LOGD_PLATFORM, "bringing up device %d", ifindex);
+    if (ifindex <= 0) {
+        /* assume success. */
+    } else {
+        if (!nm_platform_link_set_up(nm_device_get_platform(self), ifindex, no_firmware))
+            return FALSE;
+    }
+
+    /* Store carrier immediately. */
+    nm_device_set_carrier_from_platform(self);
+
+    device_is_up = nm_device_is_up(self);
+    if (block && !device_is_up) {
+        gint64 wait_until = nm_utils_get_monotonic_timestamp_usec() + 10000 /* microseconds */;
+
+        do {
+            g_usleep(200);
+            if (!nm_platform_link_refresh(nm_device_get_platform(self), ifindex))
+                return FALSE;
+            device_is_up = nm_device_is_up(self);
+        } while (!device_is_up && nm_utils_get_monotonic_timestamp_usec() < wait_until);
+    }
+
+    if (!device_is_up) {
+        if (block)
+            _LOGW(LOGD_PLATFORM, "device not up after timeout!");
+        else
+            _LOGD(LOGD_PLATFORM, "device not up immediately");
+        return FALSE;
+    }
+
+    /* some ethernet devices fail to report capabilities unless the device
+     * is up. Re-read the capabilities. */
+    capabilities = 0;
+    if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities)
+        capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self);
+    _add_capabilities(self, capabilities);
+
+    /* Devices that support carrier detect must be IFF_UP to report carrier
+     * changes; so after setting the device IFF_UP we must suppress startup
+     * complete (via a pending action) until either the carrier turns on, or
+     * a timeout is reached.
+     */
+    if (nm_device_has_capability(self, NM_DEVICE_CAP_CARRIER_DETECT)) {
+        gint64 now_ms, until_ms;
+
+        /* we start a grace period of 5 seconds during which we will schedule
+         * a pending action whenever we have no carrier.
+         *
+         * If during that time carrier goes away, we declare the interface
+         * as not ready. */
+        nm_clear_g_source(&priv->carrier_wait_id);
+        if (!priv->carrier)
+            nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+
+        now_ms   = nm_utils_get_monotonic_timestamp_msec();
+        until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
+        priv->carrier_wait_id = g_timeout_add(until_ms - now_ms, carrier_wait_timeout, self);
+    }
+
+    /* Can only get HW address of some devices when they are up */
+    nm_device_update_hw_address(self);
+
+    /* when the link comes up, we must restore IP configuration if necessary. */
+    if (priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE) {
+        if (!ip_config_merge_and_apply(self, AF_INET, TRUE))
+            _LOGW(LOGD_IP4, "failed applying IP4 config after bringing link up");
+    }
+    if (priv->ip_state_6 == NM_DEVICE_IP_STATE_DONE) {
+        if (!ip_config_merge_and_apply(self, AF_INET6, TRUE))
+            _LOGW(LOGD_IP6, "failed applying IP6 config after bringing link up");
+    }
+
+    return TRUE;
+}
+
+void
+nm_device_take_down(NMDevice *self, gboolean block)
+{
+    int      ifindex;
+    gboolean device_is_up;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    _LOGD(LOGD_PLATFORM, "taking down device %d", ifindex);
+    if (ifindex <= 0) {
+        /* devices without ifindex are always up. */
+        return;
+    }
+
+    if (!nm_platform_link_set_down(nm_device_get_platform(self), ifindex))
+        return;
+
+    device_is_up = nm_device_is_up(self);
+    if (block && device_is_up) {
+        gint64 wait_until = nm_utils_get_monotonic_timestamp_usec() + 10000 /* microseconds */;
+
+        do {
+            g_usleep(200);
+            if (!nm_platform_link_refresh(nm_device_get_platform(self), ifindex))
+                return;
+            device_is_up = nm_device_is_up(self);
+        } while (device_is_up && nm_utils_get_monotonic_timestamp_usec() < wait_until);
+    }
+
+    if (device_is_up) {
+        if (block)
+            _LOGW(LOGD_PLATFORM, "device not down after timeout!");
+        else
+            _LOGD(LOGD_PLATFORM, "device not down immediately");
+    }
+}
+
+void
+nm_device_set_firmware_missing(NMDevice *self, gboolean new_missing)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    if (priv->firmware_missing != new_missing) {
+        priv->firmware_missing = new_missing;
+        _notify(self, PROP_FIRMWARE_MISSING);
+    }
+}
+
+gboolean
+nm_device_get_firmware_missing(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->firmware_missing;
+}
+
+static void
+intersect_ext_config(NMDevice *     self,
+                     AppliedConfig *config,
+                     gboolean       intersect_addresses,
+                     gboolean       intersect_routes)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMIPConfig *     ext;
+    guint32          penalty;
+    int              family;
+
+    if (!config->orig)
+        return;
+
+    family  = nm_ip_config_get_addr_family(config->orig);
+    penalty = default_route_metric_penalty_get(self, family);
+    ext     = family == AF_INET ? (NMIPConfig *) priv->ext_ip_config_4
+                                : (NMIPConfig *) priv->ext_ip_config_6;
+
+    if (config->current) {
+        nm_ip_config_intersect(config->current,
+                               ext,
+                               intersect_addresses,
+                               intersect_routes,
+                               penalty);
+    } else {
+        config->current = nm_ip_config_intersect_alloc(config->orig,
+                                                       ext,
+                                                       intersect_addresses,
+                                                       intersect_routes,
+                                                       penalty);
+    }
+}
+
+static gboolean
+update_ext_ip_config(NMDevice *self, int addr_family, gboolean intersect_configs)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    int              ifindex;
+    GSList *         iter;
+    gboolean         is_up;
+
+    nm_assert_addr_family(addr_family);
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (!ifindex)
+        return FALSE;
+
+    is_up = nm_platform_link_is_up(nm_device_get_platform(self), ifindex);
+
+    if (NM_IS_IPv4(addr_family)) {
+        g_clear_object(&priv->ext_ip_config_4);
+        priv->ext_ip_config_4 = nm_ip4_config_capture(nm_device_get_multi_index(self),
+                                                      nm_device_get_platform(self),
+                                                      ifindex);
+        if (priv->ext_ip_config_4) {
+            if (intersect_configs) {
+                /* This function was called upon external changes. Remove the configuration
+                 * (addresses,routes) that is no longer present externally from the internal
+                 * config. This way, we don't re-add addresses that were manually removed
+                 * by the user. */
+                if (priv->con_ip_config_4) {
+                    nm_ip4_config_intersect(priv->con_ip_config_4,
+                                            priv->ext_ip_config_4,
+                                            TRUE,
+                                            is_up,
+                                            default_route_metric_penalty_get(self, AF_INET));
+                }
+
+                intersect_ext_config(self, &priv->dev_ip_config_4, TRUE, is_up);
+                intersect_ext_config(self, &priv->dev2_ip_config_4, TRUE, is_up);
+
+                for (iter = priv->vpn_configs_4; iter; iter = iter->next)
+                    nm_ip4_config_intersect(iter->data, priv->ext_ip_config_4, TRUE, is_up, 0);
+            }
+
+            /* Remove parts from ext_ip_config_4 to only contain the information that
+             * was configured externally -- we already have the same configuration from
+             * internal origins. */
+            if (priv->con_ip_config_4) {
+                nm_ip4_config_subtract(priv->ext_ip_config_4,
+                                       priv->con_ip_config_4,
+                                       default_route_metric_penalty_get(self, AF_INET));
+            }
+            if (applied_config_get_current(&priv->dev_ip_config_4)) {
+                nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_4,
+                                      applied_config_get_current(&priv->dev_ip_config_4),
+                                      default_route_metric_penalty_get(self, AF_INET));
+            }
+            if (applied_config_get_current(&priv->dev2_ip_config_4)) {
+                nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_4,
+                                      applied_config_get_current(&priv->dev2_ip_config_4),
+                                      default_route_metric_penalty_get(self, AF_INET));
+            }
+            for (iter = priv->vpn_configs_4; iter; iter = iter->next)
+                nm_ip4_config_subtract(priv->ext_ip_config_4, iter->data, 0);
+        }
+
+    } else {
+        nm_assert(!NM_IS_IPv4(addr_family));
+
+        g_clear_object(&priv->ext_ip_config_6);
+        g_clear_object(&priv->ext_ip6_config_captured);
+        priv->ext_ip6_config_captured =
+            nm_ip6_config_capture(nm_device_get_multi_index(self),
+                                  nm_device_get_platform(self),
+                                  ifindex,
+                                  NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
+        if (priv->ext_ip6_config_captured) {
+            priv->ext_ip_config_6 = nm_ip6_config_new_cloned(priv->ext_ip6_config_captured);
+
+            if (intersect_configs) {
+                /* This function was called upon external changes. Remove the configuration
+                 * (addresses,routes) that is no longer present externally from the internal
+                 * config. This way, we don't re-add addresses that were manually removed
+                 * by the user. */
+                if (priv->con_ip_config_6) {
+                    nm_ip6_config_intersect(priv->con_ip_config_6,
+                                            priv->ext_ip_config_6,
+                                            is_up,
+                                            is_up,
+                                            default_route_metric_penalty_get(self, AF_INET6));
+                }
+
+                intersect_ext_config(self, &priv->ac_ip6_config, is_up, is_up);
+                intersect_ext_config(self, &priv->dhcp6.ip6_config, is_up, is_up);
+                intersect_ext_config(self, &priv->dev2_ip_config_6, is_up, is_up);
+
+                for (iter = priv->vpn_configs_6; iter; iter = iter->next)
+                    nm_ip6_config_intersect(iter->data, priv->ext_ip_config_6, is_up, is_up, 0);
+
+                if (is_up && priv->ipv6ll_has
+                    && !nm_ip6_config_lookup_address(priv->ext_ip_config_6, &priv->ipv6ll_addr))
+                    priv->ipv6ll_has = FALSE;
+            }
+
+            /* Remove parts from ext_ip_config_6 to only contain the information that
+             * was configured externally -- we already have the same configuration from
+             * internal origins. */
+            if (priv->con_ip_config_6) {
+                nm_ip6_config_subtract(priv->ext_ip_config_6,
+                                       priv->con_ip_config_6,
+                                       default_route_metric_penalty_get(self, AF_INET6));
+            }
+            if (applied_config_get_current(&priv->ac_ip6_config)) {
+                nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6,
+                                      applied_config_get_current(&priv->ac_ip6_config),
+                                      default_route_metric_penalty_get(self, AF_INET6));
+            }
+            if (applied_config_get_current(&priv->dhcp6.ip6_config)) {
+                nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6,
+                                      applied_config_get_current(&priv->dhcp6.ip6_config),
+                                      default_route_metric_penalty_get(self, AF_INET6));
+            }
+            if (applied_config_get_current(&priv->dev2_ip_config_6)) {
+                nm_ip_config_subtract((NMIPConfig *) priv->ext_ip_config_6,
+                                      applied_config_get_current(&priv->dev2_ip_config_6),
+                                      default_route_metric_penalty_get(self, AF_INET6));
+            }
+            for (iter = priv->vpn_configs_6; iter; iter = iter->next)
+                nm_ip6_config_subtract(priv->ext_ip_config_6, iter->data, 0);
+        }
+    }
+
+    return TRUE;
+}
+
+static void
+update_ip_config(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert_addr_family(addr_family);
+
+    if (NM_IS_IPv4(addr_family))
+        priv->update_ip_config_completed_v4 = TRUE;
+    else
+        priv->update_ip_config_completed_v6 = TRUE;
+
+    if (update_ext_ip_config(self, addr_family, TRUE)) {
+        if (NM_IS_IPv4(addr_family)) {
+            if (priv->ext_ip_config_4)
+                ip_config_merge_and_apply(self, AF_INET, FALSE);
+        } else {
+            if (priv->ext_ip6_config_captured)
+                ip_config_merge_and_apply(self, AF_INET6, FALSE);
+        }
+    }
+}
+
+void
+nm_device_capture_initial_config(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->update_ip_config_completed_v4)
+        update_ip_config(self, AF_INET);
+    if (!priv->update_ip_config_completed_v6)
+        update_ip_config(self, AF_INET6);
+}
+
+static gboolean
+queued_ip_config_change(NMDevice *self, int addr_family)
+{
+    NMDevicePrivate *priv;
+    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* Wait for any queued state changes */
+    if (priv->queued_state.id)
+        return G_SOURCE_CONTINUE;
+
+    /* If a commit is scheduled, this function would potentially interfere with
+     * it changing IP configurations before they are applied. Postpone the
+     * update in such case.
+     */
+    if (priv->activation_source_id_x[IS_IPv4] != 0
+        && priv->activation_source_func_x[IS_IPv4]
+               == activate_stage5_ip_config_result_x_fcn(addr_family))
+        return G_SOURCE_CONTINUE;
+
+    priv->queued_ip_config_id_x[IS_IPv4] = 0;
+
+    update_ip_config(self, addr_family);
+
+    if (!IS_IPv4) {
+        /* Check whether we need to complete waiting for link-local.
+         * We are also called from an idle handler, so no problem doing state transitions
+         * now. */
+        linklocal6_check_complete(self);
+    }
+
+    if (!IS_IPv4) {
+        NMPlatform *          platform;
+        GSList *              dad6_failed_addrs, *iter;
+        const NMPlatformLink *pllink;
+
+        dad6_failed_addrs = g_steal_pointer(&priv->dad6_failed_addrs);
+
+        if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state < NM_DEVICE_STATE_DEACTIVATING
+            && priv->ifindex > 0 && !nm_device_sys_iface_state_is_external(self)
+            && (platform = nm_device_get_platform(self))
+            && (pllink = nm_platform_link_get(platform, priv->ifindex))
+            && (pllink->n_ifi_flags & IFF_UP)) {
+            gboolean         need_ipv6ll          = FALSE;
+            NMNDiscConfigMap ndisc_config_changed = NM_NDISC_CONFIG_NONE;
+
+            /* Handle DAD failures */
+            for (iter = dad6_failed_addrs; iter; iter = iter->next) {
+                const NMPObject *           obj = iter->data;
+                const NMPlatformIP6Address *addr;
+
+                if (!nm_ndisc_dad_addr_is_fail_candidate(platform, obj))
+                    continue;
+
+                addr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
+
+                _LOGI(LOGD_IP6,
+                      "ipv6: duplicate address check failed for the %s address",
+                      nm_platform_ip6_address_to_string(addr, NULL, 0));
+
+                if (IN6_IS_ADDR_LINKLOCAL(&addr->address))
+                    need_ipv6ll = TRUE;
+                else if (priv->ndisc)
+                    ndisc_config_changed |= nm_ndisc_dad_failed(priv->ndisc, &addr->address, FALSE);
+            }
+
+            if (ndisc_config_changed != NM_NDISC_CONFIG_NONE)
+                nm_ndisc_emit_config_change(priv->ndisc, ndisc_config_changed);
+
+            /* If no IPv6 link-local address exists but other addresses do then we
+             * must add the LL address to remain conformant with RFC 3513 chapter 2.1
+             * ("Addressing Model"): "All interfaces are required to have at least
+             * one link-local unicast address".
+             */
+            if (priv->ip_config_6 && nm_ip6_config_get_num_addresses(priv->ip_config_6))
+                need_ipv6ll = TRUE;
+            if (need_ipv6ll)
+                check_and_add_ipv6ll_addr(self);
+        }
+
+        g_slist_free_full(dad6_failed_addrs, (GDestroyNotify) nmp_object_unref);
+    }
+
+    if (!IS_IPv4) {
+        /* Check if DAD is still pending */
+        if (priv->ip_state_6 == NM_DEVICE_IP_STATE_CONF && priv->dad6_ip6_config
+            && priv->ext_ip6_config_captured
+            && !nm_ip6_config_has_any_dad_pending(priv->ext_ip6_config_captured,
+                                                  priv->dad6_ip6_config)) {
+            _LOGD(LOGD_DEVICE | LOGD_IP6, "IPv6 DAD terminated");
+            g_clear_object(&priv->dad6_ip6_config);
+            _set_ip_state(self, addr_family, NM_DEVICE_IP_STATE_DONE);
+            check_ip_state(self, FALSE, TRUE);
+            if (priv->rt6_temporary_not_available)
+                nm_device_activate_schedule_ip_config_result(self, AF_INET6, NULL);
+        }
+    }
+
+    set_unmanaged_external_down(self, TRUE);
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+queued_ip4_config_change(gpointer user_data)
+{
+    return queued_ip_config_change(user_data, AF_INET);
+}
+
+static gboolean
+queued_ip6_config_change(gpointer user_data)
+{
+    return queued_ip_config_change(user_data, AF_INET6);
+}
+
+static void
+device_ipx_changed(NMPlatform *  platform,
+                   int           obj_type_i,
+                   int           ifindex,
+                   gconstpointer platform_object,
+                   int           change_type_i,
+                   NMDevice *    self)
+{
+    const NMPObjectType              obj_type    = obj_type_i;
+    const NMPlatformSignalChangeType change_type = change_type_i;
+    NMDevicePrivate *                priv;
+    const NMPlatformIP6Address *     addr;
+
+    if (nm_device_get_ip_ifindex(self) != ifindex)
+        return;
+
+    if (!nm_device_is_real(self))
+        return;
+
+    if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
+        /* ignore all platform signals until the link is initialized in platform. */
+        return;
+    }
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    switch (obj_type) {
+    case NMP_OBJECT_TYPE_IP4_ADDRESS:
+    case NMP_OBJECT_TYPE_IP4_ROUTE:
+        if (!priv->queued_ip_config_id_4) {
+            priv->queued_ip_config_id_4 = g_idle_add(queued_ip4_config_change, self);
+            _LOGD(LOGD_DEVICE, "queued IP4 config change");
+        }
+        break;
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+        addr = platform_object;
+
+        if (priv->state > NM_DEVICE_STATE_DISCONNECTED && priv->state < NM_DEVICE_STATE_DEACTIVATING
+            && nm_ndisc_dad_addr_is_fail_candidate_event(change_type, addr)) {
+            priv->dad6_failed_addrs =
+                g_slist_prepend(priv->dad6_failed_addrs,
+                                (gpointer) nmp_object_ref(NMP_OBJECT_UP_CAST(addr)));
+        }
+
+        /* fall-through */
+    case NMP_OBJECT_TYPE_IP6_ROUTE:
+        if (!priv->queued_ip_config_id_6) {
+            priv->queued_ip_config_id_6 = g_idle_add(queued_ip6_config_change, self);
+            _LOGD(LOGD_DEVICE, "queued IP6 config change");
+        }
+        break;
+    default:
+        g_return_if_reached();
+    }
+}
+
+/*****************************************************************************/
+
+NM_UTILS_FLAGS2STR_DEFINE(nm_unmanaged_flags2str,
+                          NMUnmanagedFlags,
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_SLEEPING, "sleeping"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_QUITTING, "quitting"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_PARENT, "parent"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_TYPE, "by-type"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_PLATFORM_INIT, "platform-init"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_EXPLICIT, "user-explicit"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_BY_DEFAULT, "by-default"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_SETTINGS, "user-settings"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_CONF, "user-conf"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_USER_UDEV, "user-udev"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_EXTERNAL_DOWN, "external-down"),
+                          NM_UTILS_FLAGS2STR(NM_UNMANAGED_IS_SLAVE, "is-slave"), );
+
+static const char *
+_unmanaged_flags2str(NMUnmanagedFlags flags, NMUnmanagedFlags mask, char *buf, gsize len)
+{
+    char  buf2[512];
+    char *b;
+    char *tmp, *tmp2;
+    gsize l;
+
+    nm_utils_to_string_buffer_init(&buf, &len);
+    if (!len)
+        return buf;
+
+    b = buf;
+
+    mask |= flags;
+
+    nm_unmanaged_flags2str(flags, b, len);
+    l = strlen(b);
+    b += l;
+    len -= l;
+
+    nm_unmanaged_flags2str(mask & ~flags, buf2, sizeof(buf2));
+    if (buf2[0]) {
+        gboolean add_separator = l > 0;
+
+        tmp = buf2;
+        while (TRUE) {
+            if (add_separator)
+                nm_utils_strbuf_append_c(&b, &len, ',');
+            add_separator = TRUE;
+
+            tmp2 = strchr(tmp, ',');
+            if (tmp2)
+                tmp2[0] = '\0';
+
+            nm_utils_strbuf_append_c(&b, &len, '!');
+            nm_utils_strbuf_append_str(&b, &len, tmp);
+            if (!tmp2)
+                break;
+
+            tmp = &tmp2[1];
+        }
+    }
+
+    return buf;
+}
+
+static gboolean
+_get_managed_by_flags(NMUnmanagedFlags flags, NMUnmanagedFlags mask, gboolean for_user_request)
+{
+    /* Evaluate the managed state based on the unmanaged flags.
+     *
+     * Some flags are authoritative, meaning they always cause
+     * the device to be unmanaged (e.g. @NM_UNMANAGED_PLATFORM_INIT).
+     *
+     * OTOH, some flags can be overwritten. For example NM_UNMANAGED_USER_UDEV
+     * is ignored once NM_UNMANAGED_USER_EXPLICIT is set. The idea is that
+     * the flag from the configuration has no effect once the user explicitly
+     * touches the unmanaged flags. */
+
+    if (for_user_request) {
+        /* @for_user_request can make the result only ~more~ managed.
+         * If the flags already indicate a managed state for a non-user-request,
+         * then it is also managed for an explicit user-request.
+         *
+         * Effectively, this check is redundant, as the code below already
+         * already ensures that. Still, express this invariant explicitly here. */
+        if (_get_managed_by_flags(flags, mask, FALSE))
+            return TRUE;
+
+        /* A for-user-request, is effectively the same as pretending
+         * that user-explicit flag is cleared. */
+        mask |= NM_UNMANAGED_USER_EXPLICIT;
+        flags &= ~NM_UNMANAGED_USER_EXPLICIT;
+    }
+
+    if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_SETTINGS)
+        && !NM_FLAGS_ANY(flags, NM_UNMANAGED_USER_SETTINGS)) {
+        /* NM_UNMANAGED_USER_SETTINGS can only explicitly unmanage a device. It cannot
+         * *manage* it. Having NM_UNMANAGED_USER_SETTINGS explicitly not set, is the
+         * same as having it not set at all. */
+        mask &= ~NM_UNMANAGED_USER_SETTINGS;
+    }
+
+    if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_UDEV)) {
+        /* configuration from udev or nm-config overwrites the by-default flag
+         * which is based on the device type.
+         * configuration from udev overwrites external-down */
+        flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_EXTERNAL_DOWN);
+    }
+
+    if (NM_FLAGS_ANY(mask, NM_UNMANAGED_USER_CONF)) {
+        /* configuration from NetworkManager.conf overwrites the by-default flag
+         * which is based on the device type.
+         * It also overwrites the udev configuration and external-down */
+        flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_EXTERNAL_DOWN);
+    }
+
+    if (NM_FLAGS_HAS(mask, NM_UNMANAGED_IS_SLAVE) && !NM_FLAGS_HAS(flags, NM_UNMANAGED_IS_SLAVE)) {
+        /* for an enslaved device, by-default doesn't matter */
+        flags &= ~NM_UNMANAGED_BY_DEFAULT;
+    }
+
+    if (NM_FLAGS_HAS(mask, NM_UNMANAGED_USER_EXPLICIT)) {
+        /* if the device is managed by user-decision, certain other flags
+         * are ignored. */
+        flags &= ~(NM_UNMANAGED_BY_DEFAULT | NM_UNMANAGED_USER_UDEV | NM_UNMANAGED_USER_CONF
+                   | NM_UNMANAGED_EXTERNAL_DOWN);
+    }
+
+    return flags == NM_UNMANAGED_NONE;
+}
+
+/**
+ * nm_device_get_managed:
+ * @self: the #NMDevice
+ * @for_user_request: whether to check the flags for an explicit user-request
+ *   Setting this to %TRUE has the same effect as if %NM_UNMANAGED_USER_EXPLICIT
+ *   unmanaged flag would be unset (meaning: explicitly not-unmanaged).
+ *   If this parameter is %TRUE, the device can only appear more managed.
+ *
+ * Whether the device is unmanaged according to the unmanaged flags.
+ *
+ * Returns: %TRUE if the device is unmanaged because of the flags.
+ */
+gboolean
+nm_device_get_managed(NMDevice *self, gboolean for_user_request)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    if (!nm_device_is_real(self)) {
+        /* a unrealized device is always considered unmanaged. */
+        return FALSE;
+    }
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    return _get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, for_user_request);
+}
+
+/**
+ * nm_device_get_unmanaged_mask:
+ * @self: the #NMDevice
+ * @flag: the unmanaged flags to check.
+ *
+ * Return the unmanaged flags mask set on this device.
+ *
+ * Returns: the flags of the device ( & @flag)
+ */
+NMUnmanagedFlags
+nm_device_get_unmanaged_mask(NMDevice *self, NMUnmanagedFlags flag)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_UNMANAGED_NONE);
+    g_return_val_if_fail(flag != NM_UNMANAGED_NONE, NM_UNMANAGED_NONE);
+
+    return NM_DEVICE_GET_PRIVATE(self)->unmanaged_mask & flag;
+}
+
+/**
+ * nm_device_get_unmanaged_flags:
+ * @self: the #NMDevice
+ * @flag: the unmanaged flags to check.
+ *
+ * Return the unmanaged flags of the device.
+ *
+ * Returns: the flags of the device ( & @flag)
+ */
+NMUnmanagedFlags
+nm_device_get_unmanaged_flags(NMDevice *self, NMUnmanagedFlags flag)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_UNMANAGED_NONE);
+    g_return_val_if_fail(flag != NM_UNMANAGED_NONE, NM_UNMANAGED_NONE);
+
+    return NM_DEVICE_GET_PRIVATE(self)->unmanaged_flags & flag;
+}
+
+/**
+ * _set_unmanaged_flags:
+ * @self: the #NMDevice instance
+ * @flags: which #NMUnmanagedFlags to set.
+ * @set_op: whether to set/clear/forget the flags. You can also pass
+ *   boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED
+ *   and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively.
+ * @allow_state_transition: if %FALSE, setting flags never triggers a device
+ *   state change. If %TRUE, the device can change state, if it is real and
+ *   switches from managed to unmanaged (or vice versa).
+ * @now: whether the state change should be immediate or delayed
+ * @reason: the device state reason passed to nm_device_state_changed() if
+ *   the device becomes managed/unmanaged. This is only relevant if the
+ *   device switches state and if @allow_state_transition is %TRUE.
+ *
+ * Set the unmanaged flags of the device.
+ **/
+static void
+_set_unmanaged_flags(NMDevice *          self,
+                     NMUnmanagedFlags    flags,
+                     NMUnmanFlagOp       set_op,
+                     gboolean            allow_state_transition,
+                     gboolean            now,
+                     NMDeviceStateReason reason)
+{
+    NMDevicePrivate *priv;
+    gboolean         was_managed, transition_state;
+    NMUnmanagedFlags old_flags, old_mask;
+    NMDeviceState    new_state;
+    const char *     operation = NULL;
+    char             str1[512];
+    char             str2[512];
+    gboolean         do_notify_has_pending_actions = FALSE;
+    gboolean         had_pending_actions           = FALSE;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(flags);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->real)
+        allow_state_transition = FALSE;
+    was_managed = allow_state_transition && nm_device_get_managed(self, FALSE);
+
+    if (NM_FLAGS_HAS(priv->unmanaged_flags, NM_UNMANAGED_PLATFORM_INIT)
+        && NM_FLAGS_HAS(flags, NM_UNMANAGED_PLATFORM_INIT)
+        && NM_IN_SET(set_op, NM_UNMAN_FLAG_OP_SET_MANAGED)) {
+        /* we are clearing the platform-init flags. This triggers additional actions. */
+        if (!NM_FLAGS_HAS(flags, NM_UNMANAGED_USER_SETTINGS)) {
+            gboolean unmanaged;
+
+            unmanaged = nm_device_spec_match_list(
+                self,
+                nm_settings_get_unmanaged_specs(NM_DEVICE_GET_PRIVATE(self)->settings));
+            nm_device_set_unmanaged_flags(self, NM_UNMANAGED_USER_SETTINGS, !!unmanaged);
+        }
+
+        /* trigger an initial update of IP configuration. */
+        nm_assert_se(!nm_clear_g_source(&priv->queued_ip_config_id_4));
+        nm_assert_se(!nm_clear_g_source(&priv->queued_ip_config_id_6));
+        priv->queued_ip_config_id_4 = g_idle_add(queued_ip4_config_change, self);
+        priv->queued_ip_config_id_6 = g_idle_add(queued_ip6_config_change, self);
+
+        if (!priv->pending_actions) {
+            do_notify_has_pending_actions = TRUE;
+            had_pending_actions           = nm_device_has_pending_action(self);
+        }
+    }
+
+    old_flags = priv->unmanaged_flags;
+    old_mask  = priv->unmanaged_mask;
+
+    switch (set_op) {
+    case NM_UNMAN_FLAG_OP_FORGET:
+        priv->unmanaged_mask &= ~flags;
+        priv->unmanaged_flags &= ~flags;
+        operation = "forget";
+        break;
+    case NM_UNMAN_FLAG_OP_SET_UNMANAGED:
+        priv->unmanaged_mask |= flags;
+        priv->unmanaged_flags |= flags;
+        operation = "set-unmanaged";
+        break;
+    case NM_UNMAN_FLAG_OP_SET_MANAGED:
+        priv->unmanaged_mask |= flags;
+        priv->unmanaged_flags &= ~flags;
+        operation = "set-managed";
+        break;
+    default:
+        g_return_if_reached();
+    }
+
+    if (old_flags == priv->unmanaged_flags && old_mask == priv->unmanaged_mask)
+        return;
+
+    transition_state =
+        allow_state_transition && was_managed != nm_device_get_managed(self, FALSE)
+        && (was_managed
+            || (!was_managed && nm_device_get_state(self) == NM_DEVICE_STATE_UNMANAGED));
+
+    _LOGD(LOGD_DEVICE,
+          "unmanaged: flags set to [%s%s0x%0x/0x%x/%s%s], %s [%s=0x%0x]%s%s%s)",
+          _unmanaged_flags2str(priv->unmanaged_flags, priv->unmanaged_mask, str1, sizeof(str1)),
+          (priv->unmanaged_flags | priv->unmanaged_mask) ? "=" : "",
+          (guint) priv->unmanaged_flags,
+          (guint) priv->unmanaged_mask,
+          (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, FALSE)
+               ? "managed"
+               : (_get_managed_by_flags(priv->unmanaged_flags, priv->unmanaged_mask, TRUE)
+                      ? "manageable"
+                      : "unmanaged")),
+          priv->real ? "" : "/unrealized",
+          operation,
+          nm_unmanaged_flags2str(flags, str2, sizeof(str2)),
+          flags,
+          NM_PRINT_FMT_QUOTED(allow_state_transition,
+                              ", reason ",
+                              reason_to_string_a(reason),
+                              transition_state ? ", transition-state" : "",
+                              ""));
+
+    if (do_notify_has_pending_actions && had_pending_actions != nm_device_has_pending_action(self))
+        _notify(self, PROP_HAS_PENDING_ACTION);
+
+    if (transition_state) {
+        new_state = was_managed ? NM_DEVICE_STATE_UNMANAGED : NM_DEVICE_STATE_UNAVAILABLE;
+        if (now)
+            nm_device_state_changed(self, new_state, reason);
+        else
+            nm_device_queue_state(self, new_state, reason);
+    }
+}
+
+/**
+ * @self: the #NMDevice instance
+ * @flags: which #NMUnmanagedFlags to set.
+ * @set_op: whether to set/clear/forget the flags. You can also pass
+ *   boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED
+ *   and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively.
+ *
+ * Set the unmanaged flags of the device (does not trigger a state change).
+ **/
+void
+nm_device_set_unmanaged_flags(NMDevice *self, NMUnmanagedFlags flags, NMUnmanFlagOp set_op)
+{
+    _set_unmanaged_flags(self, flags, set_op, FALSE, FALSE, NM_DEVICE_STATE_REASON_NONE);
+}
+
+/**
+ * nm_device_set_unmanaged_by_flags:
+ * @self: the #NMDevice instance
+ * @flags: which #NMUnmanagedFlags to set.
+ * @set_op: whether to set/clear/forget the flags. You can also pass
+ *   boolean values %TRUE and %FALSE, which mean %NM_UNMAN_FLAG_OP_SET_UNMANAGED
+ *   and %NM_UNMAN_FLAG_OP_SET_MANAGED, respectively.
+ * @reason: the device state reason passed to nm_device_state_changed() if
+ *   the device becomes managed/unmanaged.
+ *
+ * Set the unmanaged flags of the device and possibly trigger a state change.
+ **/
+void
+nm_device_set_unmanaged_by_flags(NMDevice *          self,
+                                 NMUnmanagedFlags    flags,
+                                 NMUnmanFlagOp       set_op,
+                                 NMDeviceStateReason reason)
+{
+    _set_unmanaged_flags(self, flags, set_op, TRUE, TRUE, reason);
+}
+
+void
+nm_device_set_unmanaged_by_flags_queue(NMDevice *          self,
+                                       NMUnmanagedFlags    flags,
+                                       NMUnmanFlagOp       set_op,
+                                       NMDeviceStateReason reason)
+{
+    _set_unmanaged_flags(self, flags, set_op, TRUE, FALSE, reason);
+}
+
+/**
+ * nm_device_check_unrealized_device_managed:
+ *
+ * Checks if a unrealized device is managed from user settings
+ * or user configuration.
+ */
+gboolean
+nm_device_check_unrealized_device_managed(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(!nm_device_is_real(self));
+
+    if (!nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
+                                                  NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                  self,
+                                                  TRUE,
+                                                  TRUE))
+        return FALSE;
+
+    if (nm_device_spec_match_list(self, nm_settings_get_unmanaged_specs(priv->settings)))
+        return FALSE;
+
+    return TRUE;
+}
+
+void
+nm_device_set_unmanaged_by_user_settings(NMDevice *self)
+{
+    gboolean unmanaged;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    if (nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
+        /* the device is already unmanaged due to platform-init.
+         *
+         * We want to delay evaluating the device spec, because it will freeze
+         * the permanent MAC address. That should not be done, before the platform
+         * link is fully initialized (via UDEV).
+         *
+         * Note that when clearing NM_UNMANAGED_PLATFORM_INIT, we will re-evaluate
+         * whether the device is unmanaged by user-settings. */
+        return;
+    }
+
+    unmanaged = nm_device_spec_match_list(
+        self,
+        nm_settings_get_unmanaged_specs(NM_DEVICE_GET_PRIVATE(self)->settings));
+
+    nm_device_set_unmanaged_by_flags(self,
+                                     NM_UNMANAGED_USER_SETTINGS,
+                                     !!unmanaged,
+                                     unmanaged ? NM_DEVICE_STATE_REASON_NOW_UNMANAGED
+                                               : NM_DEVICE_STATE_REASON_NOW_MANAGED);
+}
+
+void
+nm_device_set_unmanaged_by_user_udev(NMDevice *self)
+{
+    int      ifindex;
+    gboolean platform_unmanaged = FALSE;
+
+    ifindex = self->_priv->ifindex;
+
+    if (ifindex <= 0
+        || !nm_platform_link_get_unmanaged(nm_device_get_platform(self),
+                                           ifindex,
+                                           &platform_unmanaged))
+        return;
+
+    nm_device_set_unmanaged_by_flags(self,
+                                     NM_UNMANAGED_USER_UDEV,
+                                     platform_unmanaged,
+                                     NM_DEVICE_STATE_REASON_USER_REQUESTED);
+}
+
+void
+nm_device_set_unmanaged_by_user_conf(NMDevice *self)
+{
+    gboolean      value;
+    NMUnmanFlagOp set_op;
+
+    value = nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
+                                                     NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                     self,
+                                                     -1,
+                                                     TRUE);
+    switch (value) {
+    case TRUE:
+        set_op = NM_UNMAN_FLAG_OP_SET_MANAGED;
+        break;
+    case FALSE:
+        set_op = NM_UNMAN_FLAG_OP_SET_UNMANAGED;
+        break;
+    default:
+        set_op = NM_UNMAN_FLAG_OP_FORGET;
+        break;
+    }
+
+    nm_device_set_unmanaged_by_flags(self,
+                                     NM_UNMANAGED_USER_CONF,
+                                     set_op,
+                                     NM_DEVICE_STATE_REASON_USER_REQUESTED);
+}
+
+void
+nm_device_set_unmanaged_by_quitting(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         need_deactivate =
+        nm_device_is_activating(self) || priv->state == NM_DEVICE_STATE_ACTIVATED;
+
+    /* It's OK to block here because we're quitting */
+    if (need_deactivate)
+        _set_state_full(self,
+                        NM_DEVICE_STATE_DEACTIVATING,
+                        NM_DEVICE_STATE_REASON_NOW_UNMANAGED,
+                        TRUE);
+
+    nm_device_set_unmanaged_by_flags(self,
+                                     NM_UNMANAGED_QUITTING,
+                                     TRUE,
+                                     need_deactivate ? NM_DEVICE_STATE_REASON_REMOVED
+                                                     : NM_DEVICE_STATE_REASON_NOW_UNMANAGED);
+}
+
+/*****************************************************************************/
+
+void
+nm_device_set_dhcp_anycast_address(NMDevice *self, const char *addr)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(!addr || nm_utils_hwaddr_valid(addr, ETH_ALEN));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    g_free(priv->dhcp_anycast_address);
+    priv->dhcp_anycast_address = g_strdup(addr);
+}
+
+void
+nm_device_reapply_settings_immediately(NMDevice *self)
+{
+    NMConnection *        applied_connection;
+    NMSettingsConnection *settings_connection;
+    NMDeviceState         state;
+    NMSettingConnection * s_con_settings;
+    NMSettingConnection * s_con_applied;
+    const char *          zone;
+    NMMetered             metered;
+    guint64               version_id;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    state = nm_device_get_state(self);
+    if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED)
+        return;
+
+    applied_connection  = nm_device_get_applied_connection(self);
+    settings_connection = nm_device_get_settings_connection(self);
+
+    if (!nm_settings_connection_has_unmodified_applied_connection(
+            settings_connection,
+            applied_connection,
+            NM_SETTING_COMPARE_FLAG_IGNORE_REAPPLY_IMMEDIATELY))
+        return;
+
+    s_con_settings = nm_connection_get_setting_connection(
+        nm_settings_connection_get_connection(settings_connection));
+    s_con_applied = nm_connection_get_setting_connection(applied_connection);
+
+    if (!nm_streq0((zone = nm_setting_connection_get_zone(s_con_settings)),
+                   nm_setting_connection_get_zone(s_con_applied))) {
+        version_id = nm_active_connection_version_id_bump(
+            (NMActiveConnection *) self->_priv->act_request.obj);
+        _LOGD(LOGD_DEVICE,
+              "reapply setting: zone = %s%s%s (version-id %llu)",
+              NM_PRINT_FMT_QUOTE_STRING(zone),
+              (unsigned long long) version_id);
+
+        g_object_set(G_OBJECT(s_con_applied), NM_SETTING_CONNECTION_ZONE, zone, NULL);
+
+        nm_device_update_firewall_zone(self);
+    }
+
+    if ((metered = nm_setting_connection_get_metered(s_con_settings))
+        != nm_setting_connection_get_metered(s_con_applied)) {
+        version_id = nm_active_connection_version_id_bump(
+            (NMActiveConnection *) self->_priv->act_request.obj);
+        _LOGD(LOGD_DEVICE,
+              "reapply setting: metered = %d (version-id %llu)",
+              (int) metered,
+              (unsigned long long) version_id);
+
+        g_object_set(G_OBJECT(s_con_applied), NM_SETTING_CONNECTION_METERED, metered, NULL);
+
+        nm_device_update_metered(self);
+    }
+}
+
+void
+nm_device_update_firewall_zone(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->fw_state >= FIREWALL_STATE_INITIALIZED
+        && !nm_device_sys_iface_state_is_external(self))
+        fw_change_zone(self);
+}
+
+void
+nm_device_update_metered(NMDevice *self)
+{
+#define NM_METERED_INVALID ((NMMetered) -1)
+    NMDevicePrivate *    priv = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingConnection *setting;
+    NMMetered            conn_value, value = NM_METERED_INVALID;
+    NMConnection *       connection = NULL;
+    NMDeviceState        state;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    state = nm_device_get_state(self);
+    if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED)
+        value = NM_METERED_UNKNOWN;
+
+    if (value == NM_METERED_INVALID) {
+        connection = nm_device_get_applied_connection(self);
+        if (connection) {
+            setting = nm_connection_get_setting_connection(connection);
+            if (setting) {
+                conn_value = nm_setting_connection_get_metered(setting);
+                if (conn_value != NM_METERED_UNKNOWN)
+                    value = conn_value;
+            }
+        }
+    }
+
+    if (value == NM_METERED_INVALID && NM_DEVICE_GET_CLASS(self)->get_guessed_metered
+        && NM_DEVICE_GET_CLASS(self)->get_guessed_metered(self))
+        value = NM_METERED_GUESS_YES;
+
+    /* Try to guess a value using the metered flag in IP configuration */
+    if (value == NM_METERED_INVALID) {
+        if (priv->ip_config_4 && priv->ip_state_4 == NM_DEVICE_IP_STATE_DONE
+            && nm_ip4_config_get_metered(priv->ip_config_4))
+            value = NM_METERED_GUESS_YES;
+    }
+
+    /* Otherwise, look at connection type. For Bluetooth, we look at the type of
+     * Bluetooth sharing: for PANU/DUN (where we are receiving internet from
+     * another device) we set GUESS_YES; for NAP (where we are sharing internet
+     * to another device) we set GUESS_NO. We ignore WiMAX here as it’s no
+     * longer supported by NetworkManager. */
+    if (value == NM_METERED_INVALID
+        && nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) {
+        if (_nm_connection_get_setting_bluetooth_for_nap(connection)) {
+            /* NAP types are not metered, but other types are. */
+            value = NM_METERED_GUESS_NO;
+        } else
+            value = NM_METERED_GUESS_YES;
+    }
+
+    if (value == NM_METERED_INVALID) {
+        if (nm_connection_is_type(connection, NM_SETTING_GSM_SETTING_NAME)
+            || nm_connection_is_type(connection, NM_SETTING_CDMA_SETTING_NAME))
+            value = NM_METERED_GUESS_YES;
+        else
+            value = NM_METERED_GUESS_NO;
+    }
+
+    if (value != priv->metered) {
+        _LOGD(LOGD_DEVICE, "set metered value %d", value);
+        priv->metered = value;
+        _notify(self, PROP_METERED);
+    }
+}
+
+static NMDeviceCheckDevAvailableFlags
+_device_check_dev_available_flags_from_con(NMDeviceCheckConAvailableFlags con_flags)
+{
+    NMDeviceCheckDevAvailableFlags dev_flags;
+
+    dev_flags = NM_DEVICE_CHECK_DEV_AVAILABLE_NONE;
+
+    if (NM_FLAGS_HAS(con_flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER))
+        dev_flags |= _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER;
+
+    return dev_flags;
+}
+
+static gboolean
+_nm_device_check_connection_available(NMDevice *                     self,
+                                      NMConnection *                 connection,
+                                      NMDeviceCheckConAvailableFlags flags,
+                                      const char *                   specific_object,
+                                      GError **                      error)
+{
+    NMDeviceState state;
+    GError *      local = NULL;
+
+    /* an unrealized software device is always available, hardware devices never. */
+    if (!nm_device_is_real(self)) {
+        if (nm_device_is_software(self)) {
+            if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) {
+                if (error) {
+                    g_return_val_if_fail(local, FALSE);
+                    nm_utils_error_set(error,
+                                       local->domain == NM_UTILS_ERROR ? local->code
+                                                                       : NM_UTILS_ERROR_UNKNOWN,
+                                       "profile is not compatible with software device (%s)",
+                                       local->message);
+                    g_error_free(local);
+                }
+                return FALSE;
+            }
+            return TRUE;
+        }
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
+                                   "hardware device is not realized");
+        return FALSE;
+    }
+
+    state = nm_device_get_state(self);
+    if (state < NM_DEVICE_STATE_UNMANAGED) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
+                                   "device is in unknown state");
+        return FALSE;
+    }
+    if (state < NM_DEVICE_STATE_UNAVAILABLE) {
+        if (nm_device_get_managed(self, FALSE)) {
+            /* device is managed, both for user-requests and non-user-requests alike. */
+        } else {
+            if (!nm_device_get_managed(self, TRUE)) {
+                /* device is strictly unmanaged by authoritative unmanaged reasons. */
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
+                                           "device is strictly unmanaged");
+                return FALSE;
+            }
+            if (!NM_FLAGS_HAS(flags,
+                              _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED)) {
+                /* device could be managed for an explict user-request, but this is not such a request. */
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_UNMANAGED_DEVICE,
+                                           "device is currently unmanaged");
+                return FALSE;
+            }
+        }
+    }
+    if (state < NM_DEVICE_STATE_DISCONNECTED && !nm_device_is_software(self)) {
+        if (!nm_device_is_available(self, _device_check_dev_available_flags_from_con(flags))) {
+            if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device is not available");
+            } else {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device is not available for internal request");
+            }
+            return FALSE;
+        }
+    }
+
+    if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) {
+        if (error) {
+            nm_utils_error_set(error,
+                               local->domain == NM_UTILS_ERROR ? local->code
+                                                               : NM_UTILS_ERROR_UNKNOWN,
+                               "profile is not compatible with device (%s)",
+                               local->message);
+            g_error_free(local);
+        }
+        return FALSE;
+    }
+
+    return NM_DEVICE_GET_CLASS(self)->check_connection_available(self,
+                                                                 connection,
+                                                                 flags,
+                                                                 specific_object,
+                                                                 error);
+}
+
+/**
+ * nm_device_check_connection_available():
+ * @self: the #NMDevice
+ * @connection: the #NMConnection to check for availability
+ * @flags: flags to affect the decision making of whether a connection
+ *   is available. Adding a flag can only make a connection more available,
+ *   not less.
+ * @specific_object: a device type dependent argument to further
+ *   filter the result. Passing a non %NULL specific object can only reduce
+ *   the availability of a connection.
+ * @error: optionally give reason why not available.
+ *
+ * Check if @connection is available to be activated on @self.
+ *
+ * Returns: %TRUE if @connection can be activated on @self
+ */
+gboolean
+nm_device_check_connection_available(NMDevice *                     self,
+                                     NMConnection *                 connection,
+                                     NMDeviceCheckConAvailableFlags flags,
+                                     const char *                   specific_object,
+                                     GError **                      error)
+{
+    gboolean available;
+
+    available =
+        _nm_device_check_connection_available(self, connection, flags, specific_object, error);
+
+#if NM_MORE_ASSERTS >= 2
+    {
+        /* The meaning of the flags is so that *adding* a flag relaxes a condition, thus making
+         * the device *more* available. Assert against that requirement by testing all the flags. */
+        NMDeviceCheckConAvailableFlags i, j, k;
+        gboolean available_all[NM_DEVICE_CHECK_CON_AVAILABLE_ALL + 1] = {FALSE};
+
+        for (i = 0; i <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; i++)
+            available_all[i] =
+                _nm_device_check_connection_available(self, connection, i, specific_object, NULL);
+
+        for (i = 0; i <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; i++) {
+            for (j = 1; j <= NM_DEVICE_CHECK_CON_AVAILABLE_ALL; j <<= 1) {
+                if (NM_FLAGS_ANY(i, j)) {
+                    k = i & ~j;
+                    nm_assert(available_all[i] == available_all[k] || available_all[i]);
+                }
+            }
+        }
+    }
+#endif
+
+    return available;
+}
+
+static gboolean
+available_connections_del_all(NMDevice *self)
+{
+    if (g_hash_table_size(self->_priv->available_connections) == 0)
+        return FALSE;
+    g_hash_table_remove_all(self->_priv->available_connections);
+    return TRUE;
+}
+
+static gboolean
+available_connections_add(NMDevice *self, NMSettingsConnection *sett_conn)
+{
+    return g_hash_table_add(self->_priv->available_connections, g_object_ref(sett_conn));
+}
+
+static gboolean
+available_connections_del(NMDevice *self, NMSettingsConnection *sett_conn)
+{
+    return g_hash_table_remove(self->_priv->available_connections, sett_conn);
+}
+
+static gboolean
+check_connection_available(NMDevice *                     self,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* Connections which require a network connection are not available when
+     * the device has no carrier, even with ignore-carrer=TRUE.
+     */
+    if (priv->carrier || !connection_requires_carrier(connection))
+        return TRUE;
+
+    if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER)
+        && priv->carrier_wait_id != 0) {
+        /* The device has no carrier though the connection requires it.
+         *
+         * If we are still waiting for carrier, the connection is available
+         * for an explicit user-request. */
+        return TRUE;
+    }
+
+    /* master types are always available even without carrier.
+     * Making connection non-available would un-enslave slaves which
+     * is not desired. */
+    if (nm_device_is_master(self))
+        return TRUE;
+
+    if (!priv->up) {
+        /* If the device is !IFF_UP it also has no carrier. But we assume that if we
+         * would start activating the device (and thereby set the device IFF_UP),
+         * that we would get a carrier. We only know after we set the device up,
+         * and we only set it up after we start activating it. So presumably, this
+         * profile would be available (but we just don't know). */
+        return TRUE;
+    }
+
+    nm_utils_error_set_literal(error,
+                               NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                               "device has no carrier");
+    return FALSE;
+}
+
+void
+nm_device_recheck_available_connections(NMDevice *self)
+{
+    NMDevicePrivate *            priv;
+    NMSettingsConnection *const *connections;
+    gboolean                     changed = FALSE;
+    GHashTableIter               h_iter;
+    NMSettingsConnection *       sett_conn;
+    guint                        i;
+    gs_unref_hashtable GHashTable *prune_list = NULL;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (g_hash_table_size(priv->available_connections) > 0) {
+        prune_list = g_hash_table_new(nm_direct_hash, NULL);
+        g_hash_table_iter_init(&h_iter, priv->available_connections);
+        while (g_hash_table_iter_next(&h_iter, (gpointer *) &sett_conn, NULL))
+            g_hash_table_add(prune_list, sett_conn);
+    }
+
+    connections = nm_settings_get_connections(priv->settings, NULL);
+    for (i = 0; connections[i]; i++) {
+        sett_conn = connections[i];
+
+        if (nm_device_check_connection_available(self,
+                                                 nm_settings_connection_get_connection(sett_conn),
+                                                 NM_DEVICE_CHECK_CON_AVAILABLE_NONE,
+                                                 NULL,
+                                                 NULL)) {
+            if (available_connections_add(self, sett_conn))
+                changed = TRUE;
+            if (prune_list)
+                g_hash_table_remove(prune_list, sett_conn);
+        }
+    }
+
+    if (prune_list) {
+        g_hash_table_iter_init(&h_iter, prune_list);
+        while (g_hash_table_iter_next(&h_iter, (gpointer *) &sett_conn, NULL)) {
+            if (available_connections_del(self, sett_conn))
+                changed = TRUE;
+        }
+    }
+
+    if (changed)
+        _notify(self, PROP_AVAILABLE_CONNECTIONS);
+    available_connections_check_delete_unrealized(self);
+}
+
+/**
+ * nm_device_get_best_connection:
+ * @self: the #NMDevice
+ * @specific_object: a specific object path if any
+ * @error: reason why no connection was returned
+ *
+ * Returns a connection that's most suitable for user-initiated activation
+ * of a device, optionally with a given specific object.
+ *
+ * Returns: the #NMSettingsConnection or %NULL (setting an @error)
+ */
+NMSettingsConnection *
+nm_device_get_best_connection(NMDevice *self, const char *specific_object, GError **error)
+{
+    NMDevicePrivate *     priv      = NM_DEVICE_GET_PRIVATE(self);
+    NMSettingsConnection *sett_conn = NULL;
+    NMSettingsConnection *candidate;
+    guint64               best_timestamp = 0;
+    GHashTableIter        iter;
+
+    g_hash_table_iter_init(&iter, priv->available_connections);
+    while (g_hash_table_iter_next(&iter, (gpointer) &candidate, NULL)) {
+        guint64 candidate_timestamp = 0;
+
+        /* If a specific object is given, only include connections that are
+         * compatible with it.
+         */
+        if (specific_object /* << Optimization: we know that the connection is available without @specific_object.  */
+            && !nm_device_check_connection_available(
+                self,
+                nm_settings_connection_get_connection(candidate),
+                _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST,
+                specific_object,
+                NULL))
+            continue;
+
+        nm_settings_connection_get_timestamp(candidate, &candidate_timestamp);
+        if (!sett_conn || (candidate_timestamp > best_timestamp)) {
+            sett_conn      = candidate;
+            best_timestamp = candidate_timestamp;
+        }
+    }
+
+    if (!sett_conn) {
+        g_set_error(error,
+                    NM_MANAGER_ERROR,
+                    NM_MANAGER_ERROR_UNKNOWN_CONNECTION,
+                    "The device '%s' has no connections available for activation.",
+                    nm_device_get_iface(self));
+    }
+
+    return sett_conn;
+}
+
+static void
+cp_connection_added_or_updated(NMDevice *self, NMSettingsConnection *sett_conn)
+{
+    gboolean changed;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+    g_return_if_fail(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    if (nm_device_check_connection_available(self,
+                                             nm_settings_connection_get_connection(sett_conn),
+                                             _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST,
+                                             NULL,
+                                             NULL))
+        changed = available_connections_add(self, sett_conn);
+    else
+        changed = available_connections_del(self, sett_conn);
+
+    if (changed) {
+        _notify(self, PROP_AVAILABLE_CONNECTIONS);
+        available_connections_check_delete_unrealized(self);
+    }
+}
+
+static void
+cp_connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data)
+{
+    cp_connection_added_or_updated(user_data, sett_conn);
+}
+
+static void
+cp_connection_updated(NMSettings *          settings,
+                      NMSettingsConnection *sett_conn,
+                      guint                 update_reason_u,
+                      gpointer              user_data)
+{
+    cp_connection_added_or_updated(user_data, sett_conn);
+}
+
+static void
+cp_connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data)
+{
+    NMDevice *self = user_data;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    if (available_connections_del(self, sett_conn)) {
+        _notify(self, PROP_AVAILABLE_CONNECTIONS);
+        available_connections_check_delete_unrealized(self);
+    }
+}
+
+gboolean
+nm_device_supports_vlans(NMDevice *self)
+{
+    return nm_platform_link_supports_vlans(nm_device_get_platform(self),
+                                           nm_device_get_ifindex(self));
+}
+
+/**
+ * nm_device_add_pending_action():
+ * @self: the #NMDevice to add the pending action to
+ * @action: a static string that identifies the action. The string instance must
+ *   stay valid until the pending action is removed (that is, the string is
+ *   not cloned, but ownership stays with the caller).
+ * @assert_not_yet_pending: if %TRUE, assert that the @action is currently not yet pending.
+ * Otherwise, ignore duplicate scheduling of the same action silently.
+ *
+ * Adds a pending action to the device.
+ *
+ * Returns: %TRUE if the action was added (and not already added before). %FALSE
+ * if the same action is already scheduled. In the latter case, the action was not scheduled
+ * a second time.
+ */
+gboolean
+nm_device_add_pending_action(NMDevice *self, const char *action, gboolean assert_not_yet_pending)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    GSList *         iter;
+    guint            count = 0;
+
+    g_return_val_if_fail(action, FALSE);
+
+    /* Check if the action is already pending. Cannot add duplicate actions */
+    for (iter = priv->pending_actions; iter; iter = iter->next) {
+        if (nm_streq(action, iter->data)) {
+            if (assert_not_yet_pending) {
+                _LOGW(LOGD_DEVICE,
+                      "add_pending_action (%d): '%s' already pending",
+                      count + g_slist_length(iter),
+                      action);
+                g_return_val_if_reached(FALSE);
+            } else {
+                _LOGT(LOGD_DEVICE,
+                      "add_pending_action (%d): '%s' already pending (expected)",
+                      count + g_slist_length(iter),
+                      action);
+            }
+            return FALSE;
+        }
+        count++;
+    }
+
+    priv->pending_actions = g_slist_prepend(priv->pending_actions, (char *) action);
+    count++;
+
+    _LOGD(LOGD_DEVICE, "add_pending_action (%d): '%s'", count, action);
+
+    if (count == 1)
+        _notify(self, PROP_HAS_PENDING_ACTION);
+
+    return TRUE;
+}
+
+/**
+ * nm_device_remove_pending_action():
+ * @self: the #NMDevice to remove the pending action from
+ * @action: a string that identifies the action.
+ * @assert_is_pending: if %TRUE, assert that the @action is pending.
+ * If %FALSE, don't do anything if the current action is not pending and
+ * return %FALSE.
+ *
+ * Removes a pending action previously added by nm_device_add_pending_action().
+ *
+ * Returns: whether the @action was pending and is now removed.
+ */
+gboolean
+nm_device_remove_pending_action(NMDevice *self, const char *action, gboolean assert_is_pending)
+{
+    NMDevicePrivate *priv;
+    GSList *         iter, *next;
+    guint            count = 0;
+
+    g_return_val_if_fail(self, FALSE);
+    g_return_val_if_fail(action, FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    for (iter = priv->pending_actions; iter; iter = next) {
+        next = iter->next;
+        if (nm_streq(action, iter->data)) {
+            _LOGD(LOGD_DEVICE,
+                  "remove_pending_action (%d): '%s'",
+                  count + g_slist_length(iter->next), /* length excluding 'iter' */
+                  action);
+            priv->pending_actions = g_slist_delete_link(priv->pending_actions, iter);
+            if (priv->pending_actions == NULL)
+                _notify(self, PROP_HAS_PENDING_ACTION);
+            return TRUE;
+        }
+        count++;
+    }
+
+    if (assert_is_pending) {
+        _LOGW(LOGD_DEVICE, "remove_pending_action (%d): '%s' not pending", count, action);
+        g_return_val_if_reached(FALSE);
+    } else
+        _LOGT(LOGD_DEVICE,
+              "remove_pending_action (%d): '%s' not pending (expected)",
+              count,
+              action);
+
+    return FALSE;
+}
+
+const char *
+nm_device_has_pending_action_reason(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->pending_actions) {
+        if (!priv->pending_actions->next && nm_device_get_state(self) == NM_DEVICE_STATE_ACTIVATED
+            && nm_streq(priv->pending_actions->data, NM_PENDING_ACTION_CARRIER_WAIT)) {
+            /* if the device is already in activated state, and the only reason
+             * why it appears still busy is "carrier-wait", then we are already complete. */
+            return NULL;
+        }
+
+        return priv->pending_actions->data;
+    }
+
+    if (nm_device_is_real(self)
+        && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
+        /* as long as the platform link is not yet initialized, we have a pending
+         * action. */
+        return NM_PENDING_ACTION_LINK_INIT;
+    }
+
+    return NULL;
+}
+
+/*****************************************************************************/
+
+static void
+_cancel_activation(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->fw_call) {
+        nm_firewall_manager_cancel_call(priv->fw_call);
+        nm_assert(!priv->fw_call);
+        priv->fw_call  = NULL;
+        priv->fw_state = FIREWALL_STATE_INITIALIZED;
+    }
+
+    dispatcher_cleanup(self);
+    ip_check_gw_ping_cleanup(self);
+
+    /* Break the activation chain */
+    activation_source_clear(self, AF_INET);
+    activation_source_clear(self, AF_INET6);
+}
+
+static void
+_cleanup_generic_pre(NMDevice *self, CleanupType cleanup_type)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    guint            i;
+
+    _cancel_activation(self);
+
+    priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    if (cleanup_type != CLEANUP_TYPE_KEEP) {
+        nm_manager_device_route_metric_clear(NM_MANAGER_GET, nm_device_get_ip_ifindex(self));
+    }
+
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE && priv->fw_state >= FIREWALL_STATE_INITIALIZED
+        && priv->fw_mgr && !nm_device_sys_iface_state_is_external(self)) {
+        nm_firewall_manager_remove_from_zone(priv->fw_mgr,
+                                             nm_device_get_ip_iface(self),
+                                             NULL,
+                                             NULL,
+                                             NULL);
+    }
+    priv->fw_state = FIREWALL_STATE_UNMANAGED;
+    g_clear_object(&priv->fw_mgr);
+
+    queued_state_clear(self);
+
+    nm_clear_pointer(&priv->shared_ip_handle, nm_netns_shared_ip_release);
+
+    for (i = 0; i < 2; i++)
+        nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free);
+
+    _cleanup_ip_pre(self, AF_INET, cleanup_type);
+    _cleanup_ip_pre(self, AF_INET6, cleanup_type);
+}
+
+static void
+_cleanup_generic_post(NMDevice *self, CleanupType cleanup_type)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->v4_commit_first_time = TRUE;
+    priv->v6_commit_first_time = TRUE;
+
+    priv->v4_route_table_initialized = FALSE;
+    priv->v6_route_table_initialized = FALSE;
+
+    priv->v4_route_table_all_sync_before = FALSE;
+    priv->v6_route_table_all_sync_before = FALSE;
+
+    priv->default_route_metric_penalty_ip4_has = FALSE;
+    priv->default_route_metric_penalty_ip6_has = FALSE;
+
+    priv->linklocal6_dad_counter = 0;
+
+    priv->mtu_force_set_done = FALSE;
+
+    /* Clean up IP configs; this does not actually deconfigure the
+     * interface; the caller must flush routes and addresses explicitly.
+     */
+    nm_device_set_ip_config(self, AF_INET, NULL, TRUE, NULL);
+    nm_device_set_ip_config(self, AF_INET6, NULL, TRUE, NULL);
+    g_clear_object(&priv->proxy_config);
+    g_clear_object(&priv->con_ip_config_4);
+    applied_config_clear(&priv->dev_ip_config_4);
+    applied_config_clear(&priv->dev2_ip_config_4);
+    g_clear_object(&priv->ext_ip_config_4);
+    g_clear_object(&priv->ip_config_4);
+    g_clear_object(&priv->con_ip_config_6);
+    applied_config_clear(&priv->ac_ip6_config);
+    g_clear_object(&priv->ext_ip_config_6);
+    g_clear_object(&priv->ext_ip6_config_captured);
+    applied_config_clear(&priv->dev2_ip_config_6);
+    g_clear_object(&priv->ip_config_6);
+    g_clear_object(&priv->dad6_ip6_config);
+    priv->ipv6ll_has = FALSE;
+    memset(&priv->ipv6ll_addr, 0, sizeof(priv->ipv6ll_addr));
+
+    nm_clear_pointer(&priv->rt6_temporary_not_available, g_hash_table_unref);
+    nm_clear_g_source(&priv->rt6_temporary_not_available_id);
+
+    g_slist_free_full(priv->vpn_configs_4, g_object_unref);
+    priv->vpn_configs_4 = NULL;
+    g_slist_free_full(priv->vpn_configs_6, g_object_unref);
+    priv->vpn_configs_6 = NULL;
+
+    /* We no longer accept the delegations. nm_device_set_ip_config(NULL)
+     * above disables them. */
+    nm_assert(priv->needs_ip6_subnet == FALSE);
+
+    if (priv->act_request.obj) {
+        nm_active_connection_set_default(NM_ACTIVE_CONNECTION(priv->act_request.obj),
+                                         AF_INET,
+                                         FALSE);
+        nm_clear_g_signal_handler(priv->act_request.obj, &priv->master_ready_id);
+        act_request_set(self, NULL);
+    }
+
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
+        /* Check if the device was deactivated, and if so, delete_link.
+         * Don't call delete_link synchronously because we are currently
+         * handling a state change -- which is not reentrant. */
+        delete_on_deactivate_check_and_schedule(self, nm_device_get_ip_ifindex(self));
+    }
+
+    /* ip_iface should be cleared after flushing all routes and addresses, since
+     * those are identified by ip_iface, not by iface (which might be a tty
+     * or ATM device).
+     */
+    _set_ip_ifindex(self, 0, NULL);
+}
+
+/*
+ * nm_device_cleanup
+ *
+ * Remove a device's routing table entries and IP addresses.
+ *
+ */
+static void
+nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanup_type)
+{
+    NMDevicePrivate *priv;
+    int              ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    if (reason == NM_DEVICE_STATE_REASON_NOW_MANAGED)
+        _LOGD(LOGD_DEVICE, "preparing device");
+    else
+        _LOGD(LOGD_DEVICE,
+              "deactivating device (reason '%s') [%d]",
+              reason_to_string_a(reason),
+              reason);
+
+    /* Save whether or not we tried IPv6 for later */
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    _cleanup_generic_pre(self, cleanup_type);
+
+    /* Turn off kernel IPv6 */
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
+        set_disable_ipv6(self, "1");
+        nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", "0");
+    }
+
+    /* Call device type-specific deactivation */
+    if (NM_DEVICE_GET_CLASS(self)->deactivate)
+        NM_DEVICE_GET_CLASS(self)->deactivate(self);
+
+    ifindex = nm_device_get_ip_ifindex(self);
+
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
+        /* master: release slaves */
+        nm_device_master_release_slaves(self);
+
+        /* Take out any entries in the routing table and any IP address the device had. */
+        if (ifindex > 0) {
+            NMPlatform *       platform = nm_device_get_platform(self);
+            NMUtilsIPv6IfaceId iid      = {};
+
+            nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex);
+            nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex);
+            nm_platform_tfilter_sync(platform, ifindex, NULL);
+            nm_platform_qdisc_sync(platform, ifindex, NULL);
+            set_ipv6_token(self, iid, "::");
+        }
+    }
+
+    _routing_rules_sync(self,
+                        cleanup_type == CLEANUP_TYPE_KEEP ? NM_TERNARY_DEFAULT : NM_TERNARY_FALSE);
+
+    if (ifindex > 0)
+        nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self), ifindex, NULL);
+
+    /* slave: mark no longer enslaved */
+    if (priv->master && priv->ifindex > 0
+        && nm_platform_link_get_master(nm_device_get_platform(self), priv->ifindex) <= 0)
+        nm_device_master_release_one_slave(priv->master,
+                                           self,
+                                           FALSE,
+                                           FALSE,
+                                           NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+
+    if (priv->lldp_listener)
+        nm_lldp_listener_stop(priv->lldp_listener);
+
+    nm_device_update_metered(self);
+
+    if (ifindex > 0) {
+        /* during device cleanup, we want to reset the MAC address of the device
+         * to the initial state.
+         *
+         * We certainly want to do that when reaching the UNMANAGED state... */
+        if (nm_device_get_state(self) <= NM_DEVICE_STATE_UNMANAGED)
+            nm_device_hw_addr_reset(self, "unmanage");
+        else {
+            /* for other device states (UNAVAILABLE, DISCONNECTED), allow the
+             * device to overwrite the reset behavior, so that Wi-Fi can set
+             * a randomized MAC address used during scanning. */
+            NM_DEVICE_GET_CLASS(self)->deactivate_reset_hw_addr(self);
+        }
+    }
+
+    priv->mtu_source = NM_DEVICE_MTU_SOURCE_NONE;
+    priv->ip6_mtu    = 0;
+    if (priv->mtu_initial || priv->ip6_mtu_initial) {
+        ifindex = nm_device_get_ip_ifindex(self);
+
+        if (ifindex > 0 && cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
+            _LOGT(LOGD_DEVICE,
+                  "mtu: reset device-mtu: %u, ipv6-mtu: %u, ifindex: %d",
+                  (guint) priv->mtu_initial,
+                  (guint) priv->ip6_mtu_initial,
+                  ifindex);
+            if (priv->mtu_initial) {
+                nm_platform_link_set_mtu(nm_device_get_platform(self), ifindex, priv->mtu_initial);
+                priv->carrier_wait_until_ms =
+                    nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+            }
+            if (priv->ip6_mtu_initial) {
+                char sbuf[64];
+
+                nm_device_sysctl_ip_conf_set(
+                    self,
+                    AF_INET6,
+                    "mtu",
+                    nm_sprintf_buf(sbuf, "%u", (unsigned) priv->ip6_mtu_initial));
+            }
+        }
+        priv->mtu_initial     = 0;
+        priv->ip6_mtu_initial = 0;
+    }
+
+    _ethtool_state_reset(self);
+
+    _cleanup_generic_post(self, cleanup_type);
+}
+
+static void
+deactivate_reset_hw_addr(NMDevice *self)
+{
+    nm_device_hw_addr_reset(self, "deactivate");
+}
+
+static char *
+find_dhcp4_address(NMDevice *self)
+{
+    NMDevicePrivate *           priv = NM_DEVICE_GET_PRIVATE(self);
+    const NMPlatformIP4Address *a;
+    NMDedupMultiIter            ipconf_iter;
+
+    if (!priv->ip_config_4)
+        return NULL;
+
+    nm_ip_config_iter_ip4_address_for_each (&ipconf_iter, priv->ip_config_4, &a) {
+        if (a->addr_source == NM_IP_CONFIG_SOURCE_DHCP)
+            return nm_utils_inet4_ntop_dup(a->address);
+    }
+    return NULL;
+}
+
+void
+nm_device_spawn_iface_helper(NMDevice *self)
+{
+    NMDevicePrivate * priv       = NM_DEVICE_GET_PRIVATE(self);
+    gboolean          configured = FALSE;
+    NMConnection *    connection;
+    GError *          error = NULL;
+    const char *      method;
+    GPtrArray *       argv;
+    gs_free char *    dhcp4_address = NULL;
+    char *            logging_backend;
+    NMUtilsStableType stable_type;
+    const char *      stable_id;
+
+    if (priv->state != NM_DEVICE_STATE_ACTIVATED)
+        return;
+    if (!nm_device_can_assume_connections(self))
+        return;
+
+    connection = nm_device_get_applied_connection(self);
+
+    g_return_if_fail(connection);
+
+    argv = g_ptr_array_sized_new(10);
+    g_ptr_array_set_free_func(argv, g_free);
+
+    g_ptr_array_add(argv, g_strdup(LIBEXECDIR "/nm-iface-helper"));
+    g_ptr_array_add(argv, g_strdup("--ifname"));
+    g_ptr_array_add(argv, g_strdup(nm_device_get_ip_iface(self)));
+    g_ptr_array_add(argv, g_strdup("--uuid"));
+    g_ptr_array_add(argv, g_strdup(nm_connection_get_uuid(connection)));
+
+    stable_id = _prop_get_connection_stable_id(self, connection, &stable_type);
+    if (stable_type != NM_UTILS_STABLE_TYPE_UUID) {
+        g_ptr_array_add(argv, g_strdup("--stable-id"));
+        g_ptr_array_add(argv, g_strdup_printf("%d %s", (int) stable_type, stable_id));
+    }
+
+    logging_backend =
+        nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG,
+                                 NM_CONFIG_KEYFILE_GROUP_LOGGING,
+                                 NM_CONFIG_KEYFILE_KEY_LOGGING_BACKEND,
+                                 NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
+    if (logging_backend) {
+        g_ptr_array_add(argv, g_strdup("--logging-backend"));
+        g_ptr_array_add(argv, logging_backend);
+    }
+
+    g_ptr_array_add(argv, g_strdup("--log-level"));
+    g_ptr_array_add(argv, g_strdup(nm_logging_level_to_string()));
+
+    g_ptr_array_add(argv, g_strdup("--log-domains"));
+    g_ptr_array_add(argv, g_strdup(nm_logging_domains_to_string()));
+
+    dhcp4_address = find_dhcp4_address(self);
+
+    method = nm_device_get_effective_ip_config_method(self, AF_INET);
+    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        NMSettingIPConfig *s_ip4;
+
+        s_ip4 = nm_connection_get_setting_ip4_config(connection);
+        nm_assert(s_ip4);
+
+        g_ptr_array_add(argv, g_strdup("--priority4"));
+        g_ptr_array_add(argv, g_strdup_printf("%u", nm_device_get_route_metric(self, AF_INET)));
+
+        g_ptr_array_add(argv, g_strdup("--dhcp4"));
+        g_ptr_array_add(argv, g_strdup(dhcp4_address));
+        if (nm_setting_ip_config_get_may_fail(s_ip4) == FALSE)
+            g_ptr_array_add(argv, g_strdup("--dhcp4-required"));
+
+        if (priv->dhcp_data_4.client) {
+            const char *hostname;
+            GBytes *    client_id;
+
+            client_id = nm_dhcp_client_get_client_id(priv->dhcp_data_4.client);
+            if (client_id) {
+                g_ptr_array_add(argv, g_strdup("--dhcp4-clientid"));
+                g_ptr_array_add(argv,
+                                nm_utils_bin2hexstr_full(g_bytes_get_data(client_id, NULL),
+                                                         g_bytes_get_size(client_id),
+                                                         ':',
+                                                         FALSE,
+                                                         NULL));
+            }
+
+            hostname = nm_dhcp_client_get_hostname(priv->dhcp_data_4.client);
+            if (hostname) {
+                if (nm_dhcp_client_get_use_fqdn(priv->dhcp_data_4.client))
+                    g_ptr_array_add(argv, g_strdup("--dhcp4-fqdn"));
+                else
+                    g_ptr_array_add(argv, g_strdup("--dhcp4-hostname"));
+                g_ptr_array_add(argv, g_strdup(hostname));
+            }
+        }
+
+        configured = TRUE;
+    }
+
+    method = nm_utils_get_ip_config_method(connection, AF_INET6);
+    if (nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+        NMSettingIPConfig *s_ip6;
+        NMUtilsIPv6IfaceId iid = NM_UTILS_IPV6_IFACE_ID_INIT;
+
+        s_ip6 = nm_connection_get_setting_ip6_config(connection);
+        g_assert(s_ip6);
+
+        g_ptr_array_add(argv, g_strdup("--priority6"));
+        g_ptr_array_add(argv, g_strdup_printf("%u", nm_device_get_route_metric(self, AF_INET6)));
+
+        g_ptr_array_add(argv, g_strdup("--slaac"));
+
+        if (nm_setting_ip_config_get_may_fail(s_ip6) == FALSE)
+            g_ptr_array_add(argv, g_strdup("--slaac-required"));
+
+        g_ptr_array_add(argv, g_strdup("--slaac-tempaddr"));
+        g_ptr_array_add(argv, g_strdup_printf("%d", priv->ndisc_use_tempaddr));
+
+        if (nm_device_get_ip_iface_identifier(self, &iid, FALSE)) {
+            g_ptr_array_add(argv, g_strdup("--iid"));
+            g_ptr_array_add(
+                argv,
+                nm_utils_bin2hexstr_full(iid.id_u8, sizeof(NMUtilsIPv6IfaceId), ':', FALSE, NULL));
+        }
+
+        g_ptr_array_add(argv, g_strdup("--addr-gen-mode"));
+        g_ptr_array_add(
+            argv,
+            g_strdup_printf("%d",
+                            nm_setting_ip6_config_get_addr_gen_mode(NM_SETTING_IP6_CONFIG(s_ip6))));
+
+        configured = TRUE;
+    }
+
+    if (configured) {
+        GPid pid;
+
+        g_ptr_array_add(argv, NULL);
+
+        if (nm_logging_enabled(LOGL_DEBUG, LOGD_DEVICE)) {
+            char *tmp;
+
+            tmp = g_strjoinv(" ", (char **) argv->pdata);
+            _LOGD(LOGD_DEVICE, "running '%s'", tmp);
+            g_free(tmp);
+        }
+
+        if (g_spawn_async(NULL,
+                          (char **) argv->pdata,
+                          NULL,
+                          G_SPAWN_DO_NOT_REAP_CHILD,
+                          NULL,
+                          NULL,
+                          &pid,
+                          &error)) {
+            _LOGI(LOGD_DEVICE, "spawned helper PID %u", (guint) pid);
+        } else {
+            _LOGW(LOGD_DEVICE, "failed to spawn helper: %s", error->message);
+            g_error_free(error);
+        }
+    }
+
+    g_ptr_array_unref(argv);
+}
+
+/*****************************************************************************/
+
+static gboolean
+ip_config_valid(NMDeviceState state)
+{
+    return (state == NM_DEVICE_STATE_UNMANAGED)
+           || (state >= NM_DEVICE_STATE_IP_CHECK && state <= NM_DEVICE_STATE_DEACTIVATING);
+}
+
+static void
+notify_ip_properties(NMDevice *self)
+{
+    _notify(self, PROP_IP_IFACE);
+    _notify(self, PROP_IP4_CONFIG);
+    _notify(self, PROP_DHCP4_CONFIG);
+    _notify(self, PROP_IP6_CONFIG);
+    _notify(self, PROP_DHCP6_CONFIG);
+}
+
+static void
+ip6_managed_setup(NMDevice *self)
+{
+    set_nm_ipv6ll(self, TRUE);
+    set_disable_ipv6(self, "1");
+    nm_device_sysctl_ip_conf_set(self, AF_INET6, "accept_ra", "0");
+    nm_device_sysctl_ip_conf_set(self, AF_INET6, "use_tempaddr", "0");
+    nm_device_sysctl_ip_conf_set(self, AF_INET6, "forwarding", "0");
+}
+
+static void
+deactivate_ready(NMDevice *self, NMDeviceStateReason reason)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->dispatcher.call_id)
+        return;
+
+    if (priv->sriov_reset_pending > 0)
+        return;
+
+    if (priv->state == NM_DEVICE_STATE_DEACTIVATING)
+        nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, reason);
+}
+
+static void
+sriov_reset_on_deactivate_cb(GError *error, gpointer user_data)
+{
+    NMDevice *       self;
+    NMDevicePrivate *priv;
+    gpointer         reason;
+
+    nm_utils_user_data_unpack(user_data, &self, &reason);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    nm_assert(priv->sriov_reset_pending > 0);
+    priv->sriov_reset_pending--;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    deactivate_ready(self, GPOINTER_TO_INT(reason));
+}
+
+static void
+sriov_reset_on_failure_cb(GError *error, gpointer user_data)
+{
+    NMDevice *       self = user_data;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert(priv->sriov_reset_pending > 0);
+    priv->sriov_reset_pending--;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    if (priv->state == NM_DEVICE_STATE_FAILED) {
+        nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_REASON_NONE);
+    }
+}
+
+static void
+deactivate_async_ready(NMDevice *self, GError *error, gpointer user_data)
+{
+    NMDevicePrivate *   priv   = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceStateReason reason = GPOINTER_TO_UINT(user_data);
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        _LOGD(LOGD_DEVICE, "Deactivation cancelled");
+        return;
+    }
+
+    g_clear_object(&priv->deactivating_cancellable);
+
+    /* In every other case, transition to the DISCONNECTED state */
+    if (error) {
+        _LOGW(LOGD_DEVICE, "Deactivation failed: %s", error->message);
+    }
+
+    deactivate_ready(self, reason);
+}
+
+static void
+deactivate_dispatcher_complete(NMDispatcherCallId *call_id, gpointer user_data)
+{
+    NMDevice *          self = NM_DEVICE(user_data);
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceStateReason reason;
+
+    g_return_if_fail(call_id == priv->dispatcher.call_id);
+    g_return_if_fail(priv->dispatcher.post_state == NM_DEVICE_STATE_DISCONNECTED);
+
+    reason = priv->state_reason;
+
+    priv->dispatcher.call_id           = NULL;
+    priv->dispatcher.post_state        = NM_DEVICE_STATE_UNKNOWN;
+    priv->dispatcher.post_state_reason = NM_DEVICE_STATE_REASON_NONE;
+
+    if (nm_clear_g_cancellable(&priv->deactivating_cancellable))
+        nm_assert_not_reached();
+
+    if (NM_DEVICE_GET_CLASS(self)->deactivate_async) {
+        /* FIXME: the virtual function deactivate_async() has only this caller here.
+         * And the NMDevice subtypes are well aware of the circumstances when they
+         * are called. We shall make the function less generic and thus (as the scope
+         * is narrower) more convenient.
+         *
+         * - Drop the callback argument. Instead, when deactivate_async() completes, the
+         *   subtype shall call a method _nm_device_deactivate_async_done(). Because as
+         *   it is currently, subtypes need to pretend this callback and the user-data
+         *   would be opaque, and carry it around. When it's in fact very clear what this
+         *   is.
+         *
+         * - Also drop the GCancellable argument. Upon cancellation, NMDevice shall
+         *   call another virtual function deactivate_async_abort(). As it is currently,
+         *   callers need to register to the cancelled signal of the cancellable. It
+         *   seems simpler to just implement the deactivate_async_abort() function.
+         *   On the other hand, some implementations actually use the GCancellable.
+         *   So, NMDevice shall do both: it shall both pass a cancellable, but also
+         *   invoke deactivate_async_abort(). It allow the implementation to honor
+         *   whatever is simpler for their purpose.
+         *
+         * - sometimes, the subclass can complete right away. Scheduling the completion
+         *   in an idle handler is cumbersome. Allow the function to return FALSE to
+         *   indicate that the device is already deactivated and the callback (or
+         *   _nm_device_deactivate_async_done()) won't be invoked.
+         */
+        priv->deactivating_cancellable = g_cancellable_new();
+        NM_DEVICE_GET_CLASS(self)->deactivate_async(self,
+                                                    priv->deactivating_cancellable,
+                                                    deactivate_async_ready,
+                                                    GUINT_TO_POINTER(reason));
+    } else
+        deactivate_ready(self, reason);
+}
+
+static void
+_set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason, gboolean quitting)
+{
+    NMDevicePrivate *priv;
+    NMDeviceState    old_state;
+    gs_unref_object NMActRequest *req         = NULL;
+    gboolean                      no_firmware = FALSE;
+    NMSettingsConnection *        sett_conn;
+    NMSettingSriov *              s_sriov;
+    gboolean                      concheck_now;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* Track re-entry */
+    g_warn_if_fail(priv->in_state_changed == FALSE);
+
+    old_state = priv->state;
+
+    if (state == NM_DEVICE_STATE_FAILED && nm_device_sys_iface_state_is_external_or_assume(self)) {
+        /* Avoid tearing down assumed connection, assume it's connected */
+        state  = NM_DEVICE_STATE_ACTIVATED;
+        reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED;
+    }
+
+    /* Do nothing if state isn't changing, but as a special case allow
+     * re-setting UNAVAILABLE if the device is missing firmware so that we
+     * can retry device initialization.
+     */
+    if ((priv->state == state)
+        && (state != NM_DEVICE_STATE_UNAVAILABLE || !priv->firmware_missing)) {
+        _LOGD(LOGD_DEVICE,
+              "state change: %s -> %s (reason '%s', sys-iface-state: '%s'%s)",
+              nm_device_state_to_str(old_state),
+              nm_device_state_to_str(state),
+              reason_to_string_a(reason),
+              _sys_iface_state_to_str(priv->sys_iface_state),
+              priv->firmware_missing ? ", missing firmware" : "");
+        return;
+    }
+
+    _LOGI(LOGD_DEVICE,
+          "state change: %s -> %s (reason '%s', sys-iface-state: '%s')",
+          nm_device_state_to_str(old_state),
+          nm_device_state_to_str(state),
+          reason_to_string_a(reason),
+          _sys_iface_state_to_str(priv->sys_iface_state));
+
+    /* in order to prevent triggering any callback caused
+     * by the device not having any pending action anymore
+     * we add one here that gets removed at the end of the function */
+    nm_device_add_pending_action(self, NM_PENDING_ACTION_IN_STATE_CHANGE, TRUE);
+    priv->in_state_changed = TRUE;
+
+    priv->state        = state;
+    priv->state_reason = reason;
+
+    queued_state_clear(self);
+
+    dispatcher_cleanup(self);
+
+    nm_clear_g_cancellable(&priv->deactivating_cancellable);
+
+    /* Cache the activation request for the dispatcher */
+    req = nm_g_object_ref(priv->act_request.obj);
+
+    if (state > NM_DEVICE_STATE_UNMANAGED && state <= NM_DEVICE_STATE_ACTIVATED
+        && nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_NOW_MANAGED
+        && NM_IN_SET_TYPED(NMDeviceSysIfaceState,
+                           priv->sys_iface_state,
+                           NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+                           NM_DEVICE_SYS_IFACE_STATE_ASSUME))
+        nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_MANAGED);
+
+    if (state <= NM_DEVICE_STATE_DISCONNECTED || state >= NM_DEVICE_STATE_ACTIVATED)
+        priv->auth_retries = NM_DEVICE_AUTH_RETRIES_UNSET;
+
+    if (state > NM_DEVICE_STATE_DISCONNECTED)
+        nm_device_assume_state_reset(self);
+
+    if (state <= NM_DEVICE_STATE_UNAVAILABLE) {
+        if (available_connections_del_all(self))
+            _notify(self, PROP_AVAILABLE_CONNECTIONS);
+        if (old_state > NM_DEVICE_STATE_UNAVAILABLE) {
+            _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED);
+        }
+    }
+
+    /* Update the available connections list when a device first becomes available */
+    if (state >= NM_DEVICE_STATE_DISCONNECTED && old_state < NM_DEVICE_STATE_DISCONNECTED)
+        nm_device_recheck_available_connections(self);
+
+    if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_DEACTIVATING) {
+        if (nm_clear_g_free(&priv->current_stable_id))
+            _LOGT(LOGD_DEVICE, "stable-id: clear");
+    }
+
+    /* Handle the new state here; but anything that could trigger
+     * another state change should be done below.
+     */
+    switch (state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+        nm_device_set_firmware_missing(self, FALSE);
+        if (old_state > NM_DEVICE_STATE_UNMANAGED) {
+            if (priv->sys_iface_state != NM_DEVICE_SYS_IFACE_STATE_MANAGED) {
+                nm_device_cleanup(self,
+                                  reason,
+                                  priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_REMOVED
+                                      ? CLEANUP_TYPE_REMOVED
+                                      : CLEANUP_TYPE_KEEP);
+            } else {
+                /* Clean up if the device is now unmanaged but was activated */
+                if (nm_device_get_act_request(self))
+                    nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
+                nm_device_take_down(self, TRUE);
+                nm_device_hw_addr_reset(self, "unmanage");
+                set_nm_ipv6ll(self, FALSE);
+                restore_ip6_properties(self);
+            }
+        }
+        nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_EXTERNAL);
+        break;
+    case NM_DEVICE_STATE_UNAVAILABLE:
+        if (old_state == NM_DEVICE_STATE_UNMANAGED) {
+            save_ip6_properties(self);
+            if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED)
+                ip6_managed_setup(self);
+            device_init_static_sriov_num_vfs(self);
+        }
+
+        if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) {
+            if (old_state == NM_DEVICE_STATE_UNMANAGED || priv->firmware_missing) {
+                if (!nm_device_bring_up(self, TRUE, &no_firmware) && no_firmware)
+                    _LOGW(LOGD_PLATFORM, "firmware may be missing.");
+                nm_device_set_firmware_missing(self, no_firmware ? TRUE : FALSE);
+            }
+
+            /* Ensure the device gets deactivated in response to stuff like
+             * carrier changes or rfkill.  But don't deactivate devices that are
+             * about to assume a connection since that defeats the purpose of
+             * assuming the device's existing connection.
+             *
+             * Note that we "deactivate" the device even when coming from
+             * UNMANAGED, to ensure that it's in a clean state.
+             */
+            nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
+        }
+        break;
+    case NM_DEVICE_STATE_DISCONNECTED:
+        if (old_state > NM_DEVICE_STATE_DISCONNECTED) {
+            /* Ensure devices that previously assumed a connection now have
+             * userspace IPv6LL enabled.
+             */
+            set_nm_ipv6ll(self, TRUE);
+
+            nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
+        } else if (old_state < NM_DEVICE_STATE_DISCONNECTED) {
+            if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) {
+                /* Ensure IPv6 is set up as it may not have been done when
+                 * entering the UNAVAILABLE state depending on the reason.
+                 */
+                ip6_managed_setup(self);
+            }
+        }
+        break;
+    case NM_DEVICE_STATE_PREPARE:
+        nm_device_update_initial_hw_address(self);
+        break;
+    case NM_DEVICE_STATE_NEED_AUTH:
+        if (old_state > NM_DEVICE_STATE_NEED_AUTH) {
+            /* Clean up any half-done IP operations if the device's layer2
+             * finds out it needs authentication during IP config.
+             */
+            _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_DECONFIGURE);
+            _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE);
+        }
+        break;
+    default:
+        break;
+    }
+
+    /* Reset intern autoconnect flags when the device is activating or connected. */
+    if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_ACTIVATED)
+        nm_device_autoconnect_blocked_unset(self, NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL);
+
+    _notify(self, PROP_STATE);
+    _notify(self, PROP_STATE_REASON);
+    nm_dbus_object_emit_signal(NM_DBUS_OBJECT(self),
+                               &interface_info_device,
+                               &signal_info_state_changed,
+                               "(uuu)",
+                               (guint32) state,
+                               (guint32) old_state,
+                               (guint32) reason);
+    g_signal_emit(self,
+                  signals[STATE_CHANGED],
+                  0,
+                  (guint) state,
+                  (guint) old_state,
+                  (guint) reason);
+
+    /* Post-process the event after internal notification */
+
+    switch (state) {
+    case NM_DEVICE_STATE_UNAVAILABLE:
+        /* If the device can activate now (ie, it's got a carrier, the supplicant
+         * is active, or whatever) schedule a delayed transition to DISCONNECTED
+         * to get things rolling.  The device can't transition immediately because
+         * we can't change states again from the state handler for a variety of
+         * reasons.
+         */
+        if (nm_device_is_available(self, NM_DEVICE_CHECK_DEV_AVAILABLE_NONE)) {
+            nm_device_queue_recheck_available(self,
+                                              NM_DEVICE_STATE_REASON_NONE,
+                                              NM_DEVICE_STATE_REASON_NONE);
+        } else {
+            _LOGD(LOGD_DEVICE, "device not yet available for transition to DISCONNECTED");
+        }
+        break;
+    case NM_DEVICE_STATE_DEACTIVATING:
+        _cancel_activation(self);
+
+        /* We cache the ignore_carrier state to not react on config-reloads while the connection
+         * is active. But on deactivating, reset the ignore-carrier flag to the current state. */
+        priv->ignore_carrier = nm_config_data_get_ignore_carrier(NM_CONFIG_GET_DATA, self);
+
+        if (quitting) {
+            nm_dispatcher_call_device_sync(NM_DISPATCHER_ACTION_PRE_DOWN, self, req);
+        } else {
+            priv->dispatcher.post_state        = NM_DEVICE_STATE_DISCONNECTED;
+            priv->dispatcher.post_state_reason = reason;
+            if (!nm_dispatcher_call_device(NM_DISPATCHER_ACTION_PRE_DOWN,
+                                           self,
+                                           req,
+                                           deactivate_dispatcher_complete,
+                                           self,
+                                           &priv->dispatcher.call_id)) {
+                /* Just proceed on errors */
+                deactivate_dispatcher_complete(0, self);
+            }
+
+            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_deactivate_cb,
+                               nm_utils_user_data_pack(self, GINT_TO_POINTER(reason)));
+            }
+        }
+
+        nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
+        break;
+    case NM_DEVICE_STATE_DISCONNECTED:
+        if (priv->queued_act_request && !priv->queued_act_request_is_waiting_for_carrier) {
+            gs_unref_object NMActRequest *queued_req = NULL;
+
+            queued_req = g_steal_pointer(&priv->queued_act_request);
+            _device_activate(self, queued_req);
+        }
+        break;
+    case NM_DEVICE_STATE_ACTIVATED:
+        _LOGI(LOGD_DEVICE, "Activation: successful, device activated.");
+        nm_device_update_metered(self);
+        nm_dispatcher_call_device(NM_DISPATCHER_ACTION_UP, self, req, NULL, NULL, NULL);
+
+        if (priv->proxy_config)
+            _pacrunner_manager_add(self);
+        break;
+    case NM_DEVICE_STATE_FAILED:
+        /* Usually upon failure the activation chain is interrupted in
+         * one of the stages; but in some cases the device fails for
+         * external events (as a failure of master connection) while
+         * the activation sequence is running and so we need to ensure
+         * that the chain is terminated here.
+         */
+        _cancel_activation(self);
+
+        sett_conn = nm_device_get_settings_connection(self);
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: failed for connection '%s'",
+              sett_conn ? nm_settings_connection_get_id(sett_conn) : "<unknown>");
+
+        /* Notify any slaves of the unexpected failure */
+        nm_device_master_release_slaves(self);
+
+        /* If the connection doesn't yet have a timestamp, set it to zero so that
+         * we can distinguish between connections we've tried to activate and have
+         * failed (zero timestamp), connections that succeeded (non-zero timestamp),
+         * and those we haven't tried yet (no timestamp).
+         */
+        if (sett_conn && !nm_settings_connection_get_timestamp(sett_conn, NULL))
+            nm_settings_connection_update_timestamp(sett_conn, (guint64) 0);
+
+        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);
+            break;
+        }
+        /* Schedule the transition to DISCONNECTED.  The device can't transition
+         * immediately because we can't change states again from the state
+         * handler for a variety of reasons.
+         */
+        nm_device_queue_state(self, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_REASON_NONE);
+        break;
+    case NM_DEVICE_STATE_IP_CHECK:
+    {
+        gboolean change_zone = FALSE;
+
+        if (!nm_device_sys_iface_state_is_external(self)) {
+            if (priv->ip_iface) {
+                /* The device now has a @ip_iface different from the
+                 * @iface on which we previously set the zone. */
+                change_zone = TRUE;
+            } else if (priv->fw_state == FIREWALL_STATE_UNMANAGED && priv->ifindex > 0) {
+                /* We didn't set the zone earlier because there was
+                 * no ifindex. */
+                change_zone = TRUE;
+            }
+        }
+
+        if (change_zone) {
+            priv->fw_state = FIREWALL_STATE_WAIT_IP_CONFIG;
+            fw_change_zone(self);
+        } else
+            nm_device_start_ip_check(self);
+
+        /* IP-related properties are only valid when the device has IP configuration;
+         * now that it does, ensure their change notifications are emitted.
+         */
+        notify_ip_properties(self);
+        break;
+    }
+    case NM_DEVICE_STATE_SECONDARIES:
+        ip_check_gw_ping_cleanup(self);
+        _LOGD(LOGD_DEVICE, "device entered SECONDARIES state");
+        break;
+    default:
+        break;
+    }
+
+    if (state > NM_DEVICE_STATE_DISCONNECTED)
+        delete_on_deactivate_unschedule(self);
+
+    if ((old_state == NM_DEVICE_STATE_ACTIVATED || old_state == NM_DEVICE_STATE_DEACTIVATING)
+        && (state != NM_DEVICE_STATE_DEACTIVATING)) {
+        if (quitting) {
+            nm_dispatcher_call_device_sync(NM_DISPATCHER_ACTION_DOWN, self, req);
+        } else {
+            nm_dispatcher_call_device(NM_DISPATCHER_ACTION_DOWN, self, req, NULL, NULL, NULL);
+        }
+    }
+
+    /* IP-related properties are only valid when the device has IP configuration.
+     * If it no longer does, ensure their change notifications are emitted.
+     */
+    if (ip_config_valid(old_state) && !ip_config_valid(state))
+        notify_ip_properties(self);
+
+    concheck_now = NM_IN_SET(state, NM_DEVICE_STATE_ACTIVATED, NM_DEVICE_STATE_DISCONNECTED)
+                   || old_state >= NM_DEVICE_STATE_ACTIVATED;
+    concheck_update_interval(self, AF_INET, concheck_now);
+    concheck_update_interval(self, AF_INET6, concheck_now);
+
+    priv->in_state_changed = FALSE;
+    nm_device_remove_pending_action(self, NM_PENDING_ACTION_IN_STATE_CHANGE, TRUE);
+
+    if ((old_state > NM_DEVICE_STATE_UNMANAGED) != (state > NM_DEVICE_STATE_UNMANAGED))
+        _notify(self, PROP_MANAGED);
+}
+
+void
+nm_device_state_changed(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason)
+{
+    _set_state_full(self, state, reason, FALSE);
+}
+
+static gboolean
+queued_state_set(gpointer user_data)
+{
+    NMDevice *          self = NM_DEVICE(user_data);
+    NMDevicePrivate *   priv = NM_DEVICE_GET_PRIVATE(self);
+    NMDeviceState       new_state;
+    NMDeviceStateReason new_reason;
+
+    nm_assert(priv->queued_state.id);
+
+    _LOGD(LOGD_DEVICE,
+          "queue-state[%s, reason:%s, id:%u]: %s",
+          nm_device_state_to_str(priv->queued_state.state),
+          reason_to_string_a(priv->queued_state.reason),
+          priv->queued_state.id,
+          "change state");
+
+    /* Clear queued state struct before triggering state change, since
+     * the state change may queue another state.
+     */
+    priv->queued_state.id = 0;
+    new_state             = priv->queued_state.state;
+    new_reason            = priv->queued_state.reason;
+
+    nm_device_state_changed(self, new_state, new_reason);
+    nm_device_remove_pending_action(self, queued_state_to_string(new_state), TRUE);
+
+    return G_SOURCE_REMOVE;
+}
+
+void
+nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason)
+{
+    NMDevicePrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE(self));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->queued_state.id && priv->queued_state.state == state) {
+        _LOGD(LOGD_DEVICE,
+              "queue-state[%s, reason:%s, id:%u]: %s%s%s%s",
+              nm_device_state_to_str(priv->queued_state.state),
+              reason_to_string_a(priv->queued_state.reason),
+              priv->queued_state.id,
+              "ignore queuing same state change",
+              NM_PRINT_FMT_QUOTED(priv->queued_state.reason != reason,
+                                  " (reason differs: ",
+                                  reason_to_string_a(reason),
+                                  ")",
+                                  ""));
+        return;
+    }
+
+    /* Add pending action for the new state before clearing the queued states, so
+     * that we don't accidentally pop all pending states and reach 'startup complete'  */
+    nm_device_add_pending_action(self, queued_state_to_string(state), TRUE);
+
+    /* We should only ever have one delayed state transition at a time */
+    if (priv->queued_state.id) {
+        _LOGW(LOGD_DEVICE,
+              "queue-state[%s, reason:%s, id:%u]: %s",
+              nm_device_state_to_str(priv->queued_state.state),
+              reason_to_string_a(priv->queued_state.reason),
+              priv->queued_state.id,
+              "replace previously queued state change");
+        nm_clear_g_source(&priv->queued_state.id);
+        nm_device_remove_pending_action(self,
+                                        queued_state_to_string(priv->queued_state.state),
+                                        TRUE);
+    }
+
+    priv->queued_state.state  = state;
+    priv->queued_state.reason = reason;
+    priv->queued_state.id     = g_idle_add(queued_state_set, self);
+
+    _LOGD(LOGD_DEVICE,
+          "queue-state[%s, reason:%s, id:%u]: %s",
+          nm_device_state_to_str(state),
+          reason_to_string_a(reason),
+          priv->queued_state.id,
+          "queue state change");
+}
+
+static void
+queued_state_clear(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->queued_state.id)
+        return;
+
+    _LOGD(LOGD_DEVICE,
+          "queue-state[%s, reason:%s, id:%u]: %s",
+          nm_device_state_to_str(priv->queued_state.state),
+          reason_to_string_a(priv->queued_state.reason),
+          priv->queued_state.id,
+          "clear queued state change");
+    nm_clear_g_source(&priv->queued_state.id);
+    nm_device_remove_pending_action(self, queued_state_to_string(priv->queued_state.state), TRUE);
+}
+
+NMDeviceState
+nm_device_get_state(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_DEVICE_STATE_UNKNOWN);
+
+    return NM_DEVICE_GET_PRIVATE(self)->state;
+}
+
+/*****************************************************************************/
+/* NMConfigDevice interface related stuff */
+
+const char *
+nm_device_get_hw_address(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+    char             buf[NM_UTILS_HWADDR_LEN_MAX];
+    gsize            l;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    nm_assert((!priv->hw_addr && priv->hw_addr_len == 0)
+              || (priv->hw_addr && _nm_utils_hwaddr_aton(priv->hw_addr, buf, sizeof(buf), &l)
+                  && l == priv->hw_addr_len));
+
+    return priv->hw_addr;
+}
+
+gboolean
+nm_device_update_hw_address(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+    const guint8 *   hwaddr;
+    gsize            hwaddrlen = 0;
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    if (priv->ifindex <= 0)
+        return FALSE;
+
+    hwaddr = nm_platform_link_get_address(nm_device_get_platform(self), priv->ifindex, &hwaddrlen);
+
+    if (priv->type == NM_DEVICE_TYPE_ETHERNET && hwaddr
+        && nm_utils_hwaddr_matches(hwaddr,
+                                   hwaddrlen,
+                                   &nm_ether_addr_zero,
+                                   sizeof(nm_ether_addr_zero)))
+        hwaddrlen = 0;
+
+    if (!hwaddrlen)
+        return FALSE;
+
+    if (priv->hw_addr_len && priv->hw_addr_len != hwaddrlen) {
+        char s_buf[NM_UTILS_HWADDR_LEN_MAX_STR];
+
+        /* we cannot change the address length of a device once it is set (except
+         * unrealizing the device).
+         *
+         * The reason is that the permanent and initial MAC addresses also must have the
+         * same address length, so it's unclear what it would mean that the length changes. */
+        _LOGD(LOGD_PLATFORM | LOGD_DEVICE,
+              "hw-addr: read a MAC address with differing length (%s vs. %s)",
+              priv->hw_addr,
+              _nm_utils_hwaddr_ntoa(hwaddr, hwaddrlen, TRUE, s_buf, sizeof(s_buf)));
+        return FALSE;
+    }
+
+    if (priv->hw_addr && nm_utils_hwaddr_matches(priv->hw_addr, -1, hwaddr, hwaddrlen))
+        return FALSE;
+
+    g_free(priv->hw_addr);
+    priv->hw_addr_len_ = hwaddrlen;
+    priv->hw_addr      = nm_utils_hwaddr_ntoa(hwaddr, hwaddrlen);
+
+    _LOGD(LOGD_PLATFORM | LOGD_DEVICE, "hw-addr: hardware address now %s", priv->hw_addr);
+    _notify(self, PROP_HW_ADDRESS);
+
+    if (!priv->hw_addr_initial
+        || (priv->hw_addr_type == HW_ADDR_TYPE_UNSET && priv->state < NM_DEVICE_STATE_PREPARE
+            && !nm_device_is_activating(self))) {
+        /* when we get a hw_addr the first time or while the device
+         * is not activated (with no explicit hw address set), always
+         * update our initial hw-address as well. */
+        nm_device_update_initial_hw_address(self);
+    }
+    return TRUE;
+}
+
+void
+nm_device_update_initial_hw_address(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->hw_addr && !nm_streq0(priv->hw_addr_initial, priv->hw_addr)) {
+        if (priv->hw_addr_initial && priv->hw_addr_type != HW_ADDR_TYPE_UNSET) {
+            /* once we have the initial hw address set, we only allow
+             * update if the currently type is "unset". */
+            return;
+        }
+        g_free(priv->hw_addr_initial);
+        priv->hw_addr_initial = g_strdup(priv->hw_addr);
+        _LOGD(LOGD_DEVICE, "hw-addr: update initial MAC address %s", priv->hw_addr_initial);
+    }
+}
+
+void
+nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_freeze)
+{
+    NMDevicePrivate *              priv = NM_DEVICE_GET_PRIVATE(self);
+    guint8                         buf[NM_UTILS_HWADDR_LEN_MAX];
+    size_t                         len = 0;
+    gboolean                       success_read;
+    int                            ifindex;
+    const NMPlatformLink *         pllink;
+    const NMConfigDeviceStateData *dev_state;
+
+    if (priv->hw_addr_perm) {
+        /* the permanent hardware address is only read once and not
+         * re-read later.
+         *
+         * Except during unrealize/realize cycles, where we clear the permanent
+         * hardware address during unrealization. */
+        return;
+    }
+
+    ifindex = priv->ifindex;
+    if (ifindex <= 0)
+        return;
+
+    /* the user is advised to configure stable MAC addresses for software devices via
+     * UDEV. Thus, check whether the link is fully initialized. */
+    pllink = nm_platform_link_get(nm_device_get_platform(self), ifindex);
+    if (!pllink || !pllink->initialized) {
+        if (!force_freeze) {
+            /* we can afford to wait. Back off and leave the permanent MAC address
+             * undecided for now. */
+            return;
+        }
+        /* try to refresh the link just to give UDEV a bit more time... */
+        nm_platform_link_refresh(nm_device_get_platform(self), ifindex);
+        /* maybe the MAC address changed... */
+        nm_device_update_hw_address(self);
+    } else if (!priv->hw_addr_len)
+        nm_device_update_hw_address(self);
+
+    if (!priv->hw_addr_len) {
+        /* we need the current MAC address because we require the permanent MAC address
+         * to have the same length as the current address.
+         *
+         * Abort if there is no current MAC address. */
+        return;
+    }
+
+    success_read =
+        nm_platform_link_get_permanent_address(nm_device_get_platform(self), ifindex, buf, &len);
+    if (success_read && priv->hw_addr_len == len) {
+        priv->hw_addr_perm_fake = FALSE;
+        priv->hw_addr_perm      = nm_utils_hwaddr_ntoa(buf, len);
+        _LOGD(LOGD_DEVICE, "hw-addr: read permanent MAC address '%s'", priv->hw_addr_perm);
+        goto notify_and_out;
+    }
+
+    /* we failed to read a permanent MAC address, thus we use a fake address,
+     * that is the current MAC address of the device.
+     *
+     * Note that the permanet MAC address of a NMDevice instance does not change
+     * after being set once. Thus, we use now a fake address and stick to that
+     * (until we unrealize the device). */
+    priv->hw_addr_perm_fake = TRUE;
+
+    /* We also persist our choice of the fake address to the device state
+     * file to use the same address on restart of NetworkManager.
+     * First, try to reload the address from the state file. */
+    dev_state = nm_config_device_state_get(nm_config_get(), ifindex);
+    if (dev_state && dev_state->perm_hw_addr_fake
+        && nm_utils_hwaddr_aton(dev_state->perm_hw_addr_fake, buf, priv->hw_addr_len)
+        && !nm_utils_hwaddr_matches(buf, priv->hw_addr_len, priv->hw_addr, -1)) {
+        _LOGD(LOGD_PLATFORM | LOGD_ETHER,
+              "hw-addr: %s (use from statefile: %s, current: %s)",
+              success_read ? "read HW addr length of permanent MAC address differs"
+                           : "unable to read permanent MAC address",
+              dev_state->perm_hw_addr_fake,
+              priv->hw_addr);
+        priv->hw_addr_perm = nm_utils_hwaddr_ntoa(buf, priv->hw_addr_len);
+        goto notify_and_out;
+    }
+
+    _LOGD(LOGD_PLATFORM | LOGD_ETHER,
+          "hw-addr: %s (use current: %s)",
+          success_read ? "read HW addr length of permanent MAC address differs"
+                       : "unable to read permanent MAC address",
+          priv->hw_addr);
+    priv->hw_addr_perm = g_strdup(priv->hw_addr);
+
+notify_and_out:
+    _notify(self, PROP_PERM_HW_ADDRESS);
+}
+
+gboolean
+nm_device_hw_addr_is_explict(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+    return !NM_IN_SET((HwAddrType) priv->hw_addr_type, HW_ADDR_TYPE_PERMANENT, HW_ADDR_TYPE_UNSET);
+}
+
+static gboolean
+_hw_addr_matches(NMDevice *self, const guint8 *addr, gsize addr_len)
+{
+    const char *cur_addr;
+
+    cur_addr = nm_device_get_hw_address(self);
+    return cur_addr && nm_utils_hwaddr_matches(addr, addr_len, cur_addr, -1);
+}
+
+static gboolean
+_hw_addr_set(NMDevice *        self,
+             const char *const addr,
+             const char *const operation,
+             const char *const detail)
+{
+    NMDevicePrivate *priv;
+    gboolean         success = FALSE;
+    int              r;
+    guint8           addr_bytes[NM_UTILS_HWADDR_LEN_MAX];
+    gsize            addr_len;
+    gboolean         was_taken_down = FALSE;
+    gboolean         retry_down;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(addr);
+    nm_assert(operation);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!_nm_utils_hwaddr_aton(addr, addr_bytes, sizeof(addr_bytes), &addr_len))
+        g_return_val_if_reached(FALSE);
+
+    /* Do nothing if current MAC is same */
+    if (_hw_addr_matches(self, addr_bytes, addr_len)) {
+        _LOGT(LOGD_DEVICE, "set-hw-addr: no MAC address change needed (%s)", addr);
+        return TRUE;
+    }
+
+    if (priv->hw_addr_len && priv->hw_addr_len != addr_len) {
+        _LOGT(LOGD_DEVICE,
+              "set-hw-addr: setting MAC address to '%s' (%s, %s) failed because of wrong address "
+              "length (should be %u bytes)",
+              addr,
+              operation,
+              detail,
+              priv->hw_addr_len);
+        return FALSE;
+    }
+
+    _LOGT(LOGD_DEVICE,
+          "set-hw-addr: setting MAC address to '%s' (%s, %s)...",
+          addr,
+          operation,
+          detail);
+
+    if (nm_device_get_device_type(self) == NM_DEVICE_TYPE_WIFI) {
+        /* Always take the device down for Wi-Fi because
+         * wpa_supplicant needs it to properly detect the MAC
+         * change. */
+        retry_down     = FALSE;
+        was_taken_down = TRUE;
+        nm_device_take_down(self, FALSE);
+    }
+
+again:
+    r       = nm_platform_link_set_address(nm_device_get_platform(self),
+                                     nm_device_get_ip_ifindex(self),
+                                     addr_bytes,
+                                     addr_len);
+    success = (r >= 0);
+    if (!success) {
+        retry_down =
+            !was_taken_down && r != -NME_PL_NOT_FOUND
+            && nm_platform_link_is_up(nm_device_get_platform(self), nm_device_get_ip_ifindex(self));
+        _NMLOG((retry_down || r == -NME_PL_NOT_FOUND) ? LOGL_DEBUG : LOGL_WARN,
+               LOGD_DEVICE,
+               "set-hw-addr: failed to %s MAC address to %s (%s) (%s)%s",
+               operation,
+               addr,
+               detail,
+               nm_strerror(r),
+               retry_down ? " (retry with taking down)" : "");
+    } else {
+        /* MAC address successfully changed; update the current MAC to match */
+        nm_device_update_hw_address(self);
+
+        if (!_hw_addr_matches(self, addr_bytes, addr_len)) {
+            gint64 poll_end, now;
+
+            _LOGD(LOGD_DEVICE,
+                  "set-hw-addr: new MAC address %s not successfully %s (%s) (refresh link)",
+                  addr,
+                  operation,
+                  detail);
+
+            /* The platform call indicated success, however the address is not
+             * as expected. That is either due to a driver issue (brcmfmac, bgo#770456,
+             * rh#1374023) or a race where externally the MAC address was reset.
+             * The race is rather unlikely.
+             *
+             * The alternative would be to postpone the activation in case the
+             * MAC address is not yet ready and poll without blocking. However,
+             * that is rather complicated and it is not expected that this case
+             * happens for regular drivers.
+             * Note that brcmfmac can block NetworkManager for 500 msec while
+             * taking down the device. Let's add another 100 msec to that.
+             *
+             * wait/poll up to 100 msec until it changes. */
+
+            poll_end = nm_utils_get_monotonic_timestamp_usec() + (100 * 1000);
+            for (;;) {
+                if (!nm_platform_link_refresh(nm_device_get_platform(self),
+                                              nm_device_get_ip_ifindex(self)))
+                    goto handle_fail;
+                if (!nm_device_update_hw_address(self))
+                    goto handle_wait;
+                if (!_hw_addr_matches(self, addr_bytes, addr_len))
+                    goto handle_fail;
+
+                break;
+handle_wait:
+                now = nm_utils_get_monotonic_timestamp_usec();
+                if (now < poll_end) {
+                    g_usleep(NM_MIN(poll_end - now, 500));
+                    continue;
+                }
+handle_fail:
+                success = FALSE;
+                break;
+            }
+        }
+
+        if (success) {
+            retry_down = FALSE;
+            _LOGI(LOGD_DEVICE, "set-hw-addr: %s MAC address to %s (%s)", operation, addr, detail);
+        } else {
+            retry_down = !was_taken_down
+                         && nm_platform_link_is_up(nm_device_get_platform(self),
+                                                   nm_device_get_ip_ifindex(self));
+
+            _NMLOG(retry_down ? LOGL_DEBUG : LOGL_WARN,
+                   LOGD_DEVICE,
+                   "set-hw-addr: new MAC address %s not successfully %s (%s)%s",
+                   addr,
+                   operation,
+                   detail,
+                   retry_down ? " (retry with taking down)" : "");
+        }
+    }
+
+    if (retry_down) {
+        /* changing the MAC address failed, but also the device was up (and we did not yet try to take
+         * it down). Optimally, we change the MAC address without taking the device down, but some
+         * devices don't like that. So, retry with taking the device down. */
+        retry_down     = FALSE;
+        was_taken_down = TRUE;
+        nm_device_take_down(self, FALSE);
+        goto again;
+    }
+
+    if (was_taken_down) {
+        if (!nm_device_bring_up(self, TRUE, NULL))
+            return FALSE;
+    }
+
+    return success;
+}
+
+gboolean
+nm_device_hw_addr_set(NMDevice *self, const char *addr, const char *detail, gboolean set_permanent)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!addr)
+        g_return_val_if_reached(FALSE);
+
+    if (set_permanent) {
+        /* The type is set to PERMANENT by NMDeviceVlan when taking the MAC
+         * address from the parent and by NMDeviceWifi when setting a random MAC
+         * address during scanning.
+         */
+        priv->hw_addr_type = HW_ADDR_TYPE_PERMANENT;
+    }
+
+    return _hw_addr_set(self, addr, "set", detail);
+}
+
+/*
+ * _hw_addr_get_cloned:
+ * @self: a #NMDevice
+ * @connection: a #NMConnection
+ * @is_wifi: whether the device is Wi-Fi
+ * @preserve: (out): whether the address must be reset to initial one
+ * @hwaddr: (out): the cloned MAC address to set on interface
+ * @hwaddr_type: (out): the type of address to set
+ * @hwaddr_detail: (out): the detail (origin) of address to set
+ * @error: (out): on return, an error or %NULL
+ *
+ * Computes the MAC to be set on a interface. On success, one of the
+ * following exclusive conditions are verified:
+ *
+ *  - @preserve is %TRUE: the address must be reset to the initial one
+ *  - @hwaddr is not %NULL: the given address must be set on the device
+ *  - @hwaddr is %NULL and @preserve is %FALSE: no action needed
+ *
+ * Returns: %FALSE in case of error in determining the cloned MAC address,
+ * %TRUE otherwise
+ */
+static gboolean
+_hw_addr_get_cloned(NMDevice *    self,
+                    NMConnection *connection,
+                    gboolean      is_wifi,
+                    gboolean *    preserve,
+                    char **       hwaddr,
+                    HwAddrType *  hwaddr_type,
+                    char **       hwaddr_detail,
+                    GError **     error)
+{
+    NMDevicePrivate *priv;
+    gs_free char *   addr_setting_free             = NULL;
+    gs_free char *   hw_addr_generated             = NULL;
+    gs_free char *   generate_mac_address_mask_tmp = NULL;
+    const char *     addr, *addr_setting;
+    char *           addr_out;
+    HwAddrType       type_out;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!connection)
+        g_return_val_if_reached(FALSE);
+
+    addr = addr_setting =
+        _prop_get_x_cloned_mac_address(self, connection, is_wifi, &addr_setting_free);
+
+    if (nm_streq(addr, NM_CLONED_MAC_PRESERVE)) {
+        /* "preserve" means to reset the initial MAC address. */
+        NM_SET_OUT(preserve, TRUE);
+        NM_SET_OUT(hwaddr, NULL);
+        NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET);
+        NM_SET_OUT(hwaddr_detail, g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting));
+        return TRUE;
+    }
+
+    if (nm_streq(addr, NM_CLONED_MAC_PERMANENT)) {
+        gboolean is_fake;
+
+        addr = nm_device_get_permanent_hw_address_full(self, TRUE, &is_fake);
+        if (is_fake) {
+            /* Preserve the current address if the permanent address if fake */
+            NM_SET_OUT(preserve, TRUE);
+            NM_SET_OUT(hwaddr, NULL);
+            NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET);
+            NM_SET_OUT(hwaddr_detail,
+                       g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting));
+            return TRUE;
+        } else if (!addr) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_FAILED,
+                                "failed to retrieve permanent address");
+            return FALSE;
+        }
+        addr_out = g_strdup(addr);
+        type_out = HW_ADDR_TYPE_PERMANENT;
+    } else if (NM_IN_STRSET(addr, NM_CLONED_MAC_RANDOM)) {
+        if (priv->hw_addr_type == HW_ADDR_TYPE_GENERATED) {
+            /* hm, we already use a generate MAC address. Most certainly, that is from the same
+             * activation request, so we should not create a new random address, instead keep
+             * the current. */
+            goto out_no_action;
+        }
+        hw_addr_generated = nm_utils_hw_addr_gen_random_eth(
+            nm_device_get_initial_hw_address(self),
+            _prop_get_x_generate_mac_address_mask(self,
+                                                  connection,
+                                                  is_wifi,
+                                                  &generate_mac_address_mask_tmp));
+        if (!hw_addr_generated) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "failed to generate %s MAC address",
+                        "random");
+            return FALSE;
+        }
+
+        addr_out = g_steal_pointer(&hw_addr_generated);
+        type_out = HW_ADDR_TYPE_GENERATED;
+    } else if (NM_IN_STRSET(addr, NM_CLONED_MAC_STABLE)) {
+        NMUtilsStableType stable_type;
+        const char *      stable_id;
+
+        if (priv->hw_addr_type == HW_ADDR_TYPE_GENERATED) {
+            /* hm, we already use a generate MAC address. Most certainly, that is from the same
+             * activation request, so let's skip creating the stable address anew. */
+            goto out_no_action;
+        }
+
+        stable_id         = _prop_get_connection_stable_id(self, connection, &stable_type);
+        hw_addr_generated = nm_utils_hw_addr_gen_stable_eth(
+            stable_type,
+            stable_id,
+            nm_device_get_ip_iface(self),
+            nm_device_get_initial_hw_address(self),
+            _prop_get_x_generate_mac_address_mask(self,
+                                                  connection,
+                                                  is_wifi,
+                                                  &generate_mac_address_mask_tmp));
+        if (!hw_addr_generated) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_FAILED,
+                        "failed to generate %s MAC address",
+                        "stable");
+            return FALSE;
+        }
+
+        addr_out = g_steal_pointer(&hw_addr_generated);
+        type_out = HW_ADDR_TYPE_GENERATED;
+    } else {
+        /* this must be a valid address. Otherwise, we shouldn't come here. */
+        if (!nm_utils_hwaddr_valid(addr, -1))
+            g_return_val_if_reached(FALSE);
+
+        addr_out = g_strdup(addr);
+        type_out = HW_ADDR_TYPE_EXPLICIT;
+    }
+
+    NM_SET_OUT(preserve, FALSE);
+    NM_SET_OUT(hwaddr, addr_out);
+    NM_SET_OUT(hwaddr_type, type_out);
+    NM_SET_OUT(hwaddr_detail, g_steal_pointer(&addr_setting_free) ?: g_strdup(addr_setting));
+    return TRUE;
+out_no_action:
+    NM_SET_OUT(preserve, FALSE);
+    NM_SET_OUT(hwaddr, NULL);
+    NM_SET_OUT(hwaddr_type, HW_ADDR_TYPE_UNSET);
+    NM_SET_OUT(hwaddr_detail, NULL);
+    return TRUE;
+}
+
+gboolean
+nm_device_hw_addr_get_cloned(NMDevice *    self,
+                             NMConnection *connection,
+                             gboolean      is_wifi,
+                             char **       hwaddr,
+                             gboolean *    preserve,
+                             GError **     error)
+{
+    if (!_hw_addr_get_cloned(self, connection, is_wifi, preserve, hwaddr, NULL, NULL, error))
+        return FALSE;
+
+    return TRUE;
+}
+
+gboolean
+nm_device_hw_addr_set_cloned(NMDevice *self, NMConnection *connection, gboolean is_wifi)
+{
+    NMDevicePrivate *priv;
+    gboolean         preserve   = FALSE;
+    gs_free char *   hwaddr     = NULL;
+    gs_free char *   detail     = NULL;
+    HwAddrType       type       = HW_ADDR_TYPE_UNSET;
+    gs_free_error GError *error = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!_hw_addr_get_cloned(self,
+                             connection,
+                             is_wifi,
+                             &preserve,
+                             &hwaddr,
+                             &type,
+                             &detail,
+                             &error)) {
+        _LOGW(LOGD_DEVICE, "set-hw-addr: %s", error->message);
+        return FALSE;
+    }
+
+    if (preserve)
+        return nm_device_hw_addr_reset(self, detail);
+
+    if (hwaddr) {
+        priv->hw_addr_type = type;
+        return _hw_addr_set(self, hwaddr, "set-cloned", detail);
+    }
+
+    return TRUE;
+}
+
+gboolean
+nm_device_hw_addr_reset(NMDevice *self, const char *detail)
+{
+    NMDevicePrivate *priv;
+    const char *     addr;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->hw_addr_type == HW_ADDR_TYPE_UNSET)
+        return TRUE;
+
+    priv->hw_addr_type = HW_ADDR_TYPE_UNSET;
+    addr               = nm_device_get_initial_hw_address(self);
+    if (!addr) {
+        /* as hw_addr_type is not UNSET, we expect that we can get an
+         * initial address to which to reset. */
+        g_return_val_if_reached(FALSE);
+    }
+
+    return _hw_addr_set(self, addr, "reset", detail);
+}
+
+const char *
+nm_device_get_permanent_hw_address_full(NMDevice *self,
+                                        gboolean  force_freeze,
+                                        gboolean *out_is_fake)
+{
+    NMDevicePrivate *priv;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), ({
+                             NM_SET_OUT(out_is_fake, FALSE);
+                             NULL;
+                         }));
+
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (!priv->hw_addr_perm && force_freeze) {
+        /* somebody requests a permanent MAC address, but we don't have it set
+         * yet. We cannot delay it any longer and try to get it without waiting
+         * for UDEV. */
+        nm_device_update_permanent_hw_address(self, TRUE);
+    }
+
+    NM_SET_OUT(out_is_fake, priv->hw_addr_perm && priv->hw_addr_perm_fake);
+    return priv->hw_addr_perm;
+}
+
+const char *
+nm_device_get_permanent_hw_address(NMDevice *self)
+{
+    return nm_device_get_permanent_hw_address_full(self, TRUE, NULL);
+}
+
+const char *
+nm_device_get_initial_hw_address(NMDevice *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    return NM_DEVICE_GET_PRIVATE(self)->hw_addr_initial;
+}
+
+/**
+ * nm_device_spec_match_list:
+ * @self: an #NMDevice
+ * @specs: (element-type utf8): a list of device specs
+ *
+ * Checks if @self matches any of the specifications in @specs. The
+ * currently-supported spec types are:
+ *
+ *     "mac:00:11:22:33:44:55" - matches a device with the given
+ *     hardware address
+ *
+ *     "interface-name:foo0" - matches a device with the given
+ *     interface name
+ *
+ *     "s390-subchannels:00.11.22" - matches a device with the given
+ *     z/VM / s390 subchannels.
+ *
+ *     "*" - matches any device
+ *
+ * Returns: #TRUE if @self matches one of the specs in @specs
+ */
+gboolean
+nm_device_spec_match_list(NMDevice *self, const GSList *specs)
+{
+    return nm_device_spec_match_list_full(self, specs, FALSE);
+}
+
+int
+nm_device_spec_match_list_full(NMDevice *self, const GSList *specs, int no_match_value)
+{
+    NMDeviceClass *      klass;
+    NMMatchSpecMatchType m;
+    const char *         hw_address = NULL;
+    gboolean             is_fake;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    klass      = NM_DEVICE_GET_CLASS(self);
+    hw_address = nm_device_get_permanent_hw_address_full(
+        self,
+        !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT),
+        &is_fake);
+
+    m = nm_match_spec_device(specs,
+                             nm_device_get_iface(self),
+                             nm_device_get_type_description(self),
+                             nm_device_get_driver(self),
+                             nm_device_get_driver_version(self),
+                             is_fake ? NULL : hw_address,
+                             klass->get_s390_subchannels ? klass->get_s390_subchannels(self) : NULL,
+                             nm_dhcp_manager_get_config(nm_dhcp_manager_get()));
+
+    switch (m) {
+    case NM_MATCH_SPEC_MATCH:
+        return TRUE;
+    case NM_MATCH_SPEC_NEG_MATCH:
+        return FALSE;
+    case NM_MATCH_SPEC_NO_MATCH:
+        return no_match_value;
+    }
+    nm_assert_not_reached();
+    return no_match_value;
+}
+
+guint
+nm_device_get_supplicant_timeout(NMDevice *self)
+{
+    NMConnection *  connection;
+    NMSetting8021x *s_8021x;
+    int             timeout;
+#define SUPPLICANT_DEFAULT_TIMEOUT 25
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), SUPPLICANT_DEFAULT_TIMEOUT);
+
+    connection = nm_device_get_applied_connection(self);
+
+    g_return_val_if_fail(connection, SUPPLICANT_DEFAULT_TIMEOUT);
+
+    s_8021x = nm_connection_get_setting_802_1x(connection);
+    if (s_8021x) {
+        timeout = nm_setting_802_1x_get_auth_timeout(s_8021x);
+        if (timeout > 0)
+            return timeout;
+    }
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("802-1x.auth-timeout"),
+                                                       self,
+                                                       1,
+                                                       G_MAXINT32,
+                                                       SUPPLICANT_DEFAULT_TIMEOUT);
+}
+
+gboolean
+nm_device_auth_retries_try_next(NMDevice *self)
+{
+    NMDevicePrivate *    priv;
+    NMSettingConnection *s_con;
+    int                  auth_retries;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+
+    priv         = NM_DEVICE_GET_PRIVATE(self);
+    auth_retries = priv->auth_retries;
+
+    if (G_UNLIKELY(auth_retries == NM_DEVICE_AUTH_RETRIES_UNSET)) {
+        auth_retries = -1;
+
+        s_con = nm_device_get_applied_setting(self, NM_TYPE_SETTING_CONNECTION);
+        if (s_con)
+            auth_retries = nm_setting_connection_get_auth_retries(s_con);
+
+        if (auth_retries == -1) {
+            auth_retries = nm_config_data_get_connection_default_int64(
+                NM_CONFIG_GET_DATA,
+                NM_CON_DEFAULT("connection.auth-retries"),
+                self,
+                -1,
+                G_MAXINT32,
+                -1);
+        }
+
+        if (auth_retries == 0)
+            auth_retries = NM_DEVICE_AUTH_RETRIES_INFINITY;
+        else if (auth_retries == -1)
+            auth_retries = NM_DEVICE_AUTH_RETRIES_DEFAULT;
+        else
+            nm_assert(auth_retries > 0);
+
+        priv->auth_retries = auth_retries;
+    }
+
+    if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY)
+        return TRUE;
+    if (auth_retries <= 0) {
+        nm_assert(auth_retries == 0);
+        return FALSE;
+    }
+    priv->auth_retries--;
+    return TRUE;
+}
+
+static void
+hostname_dns_lookup_callback(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    HostnameResolver *resolver;
+    NMDevice *        self;
+    gs_free char *    hostname  = NULL;
+    gs_free char *    addr_str  = NULL;
+    gs_free_error GError *error = NULL;
+
+    hostname = g_resolver_lookup_by_address_finish(G_RESOLVER(source), result, &error);
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    resolver           = user_data;
+    self               = resolver->device;
+    resolver->state    = RESOLVER_DONE;
+    resolver->hostname = g_strdup(hostname);
+
+    _LOGD(LOGD_DNS,
+          "hostname-from-dns: lookup done for %s, result %s%s%s",
+          (addr_str = g_inet_address_to_string(resolver->address)),
+          NM_PRINT_FMT_QUOTE_STRING(hostname));
+
+    nm_clear_g_cancellable(&resolver->cancellable);
+    g_signal_emit(self, signals[DNS_LOOKUP_DONE], 0);
+}
+
+static gboolean
+hostname_dns_address_timeout(gpointer user_data)
+{
+    HostnameResolver *resolver = user_data;
+    NMDevice *        self     = resolver->device;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), G_SOURCE_REMOVE);
+
+    nm_assert(resolver->state == RESOLVER_WAIT_ADDRESS);
+    nm_assert(!resolver->address);
+    nm_assert(!resolver->cancellable);
+
+    _LOGT(LOGD_DNS,
+          "hostname-from-dns: timed out while waiting IPv%c address",
+          nm_utils_addr_family_to_char(resolver->addr_family));
+
+    resolver->timeout_id = 0;
+    resolver->state      = RESOLVER_DONE;
+    g_signal_emit(self, signals[DNS_LOOKUP_DONE], 0);
+
+    return G_SOURCE_REMOVE;
+}
+
+static const char *
+_resolver_state_to_string(ResolverState state)
+{
+    switch (state) {
+    case RESOLVER_WAIT_ADDRESS:
+        return "wait-address";
+    case RESOLVER_IN_PROGRESS:
+        return "in-progress";
+    case RESOLVER_DONE:
+        return "done";
+    default:
+        nm_assert_not_reached();
+        return "unknown";
+    }
+}
+
+void
+nm_device_clear_dns_lookup_data(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    guint            i;
+
+    for (i = 0; i < 2; i++)
+        nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free);
+}
+
+/* return value is valid only immediately */
+const char *
+nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean *out_wait)
+{
+    NMDevicePrivate * priv;
+    const int         IS_IPv4 = NM_IS_IPv4(addr_family);
+    HostnameResolver *resolver;
+    NMIPConfig *      ip_config;
+    const char *      method;
+    gboolean          address_changed         = FALSE;
+    gs_unref_object GInetAddress *new_address = NULL;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    /* If the device is not supposed to have addresses,
+     * return an immediate empty result.*/
+    if (!nm_device_get_applied_connection(self)) {
+        NM_SET_OUT(out_wait, FALSE);
+        return NULL;
+    }
+
+    method = nm_device_get_effective_ip_config_method(self, addr_family);
+    if (IS_IPv4) {
+        if (NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED)) {
+            nm_clear_pointer(&priv->hostname_resolver_x[IS_IPv4], _hostname_resolver_free);
+            NM_SET_OUT(out_wait, FALSE);
+            return NULL;
+        }
+    } else {
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
+                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE)) {
+            nm_clear_pointer(&priv->hostname_resolver_x[IS_IPv4], _hostname_resolver_free);
+            NM_SET_OUT(out_wait, FALSE);
+            return NULL;
+        }
+    }
+
+    resolver = priv->hostname_resolver_x[IS_IPv4];
+    if (!resolver) {
+        resolver  = g_slice_new(HostnameResolver);
+        *resolver = (HostnameResolver){
+            .resolver    = g_resolver_get_default(),
+            .device      = self,
+            .addr_family = addr_family,
+            .state       = RESOLVER_WAIT_ADDRESS,
+        };
+        priv->hostname_resolver_x[IS_IPv4] = resolver;
+    }
+
+    /* Determine the first address of the interface and
+     * whether it changed from the previous lookup */
+    ip_config = priv->ip_config_x[IS_IPv4];
+    if (ip_config) {
+        const NMPlatformIPAddress *addr;
+
+        addr = nm_ip_config_get_first_address(ip_config);
+        if (addr) {
+            new_address = g_inet_address_new_from_bytes(addr->address_ptr,
+                                                        IS_IPv4 ? G_SOCKET_FAMILY_IPV4
+                                                                : G_SOCKET_FAMILY_IPV6);
+        }
+    }
+
+    if (new_address && resolver->address) {
+        if (!g_inet_address_equal(new_address, resolver->address))
+            address_changed = TRUE;
+    } else if (new_address != resolver->address)
+        address_changed = TRUE;
+
+    {
+        gs_free char *old_str = NULL;
+        gs_free char *new_str = NULL;
+
+        _LOGT(LOGD_DNS,
+              "hostname-from-dns: ipv%c resolver state %s, old address %s, new address %s",
+              nm_utils_addr_family_to_char(resolver->addr_family),
+              _resolver_state_to_string(resolver->state),
+              resolver->address ? (old_str = g_inet_address_to_string(resolver->address))
+                                : "(null)",
+              new_address ? (new_str = g_inet_address_to_string(new_address)) : "(null)");
+    }
+
+    /* In every state, if the address changed, we restart
+     * the resolution with the new address */
+    if (address_changed) {
+        nm_clear_g_cancellable(&resolver->cancellable);
+        g_clear_object(&resolver->address);
+        resolver->state = RESOLVER_WAIT_ADDRESS;
+    }
+
+    if (address_changed && new_address) {
+        gs_free char *str = NULL;
+
+        _LOGT(LOGD_DNS,
+              "hostname-from-dns: starting lookup for address %s",
+              (str = g_inet_address_to_string(new_address)));
+
+        resolver->state       = RESOLVER_IN_PROGRESS;
+        resolver->cancellable = g_cancellable_new();
+        resolver->address     = g_steal_pointer(&new_address);
+        g_resolver_lookup_by_address_async(resolver->resolver,
+                                           resolver->address,
+                                           resolver->cancellable,
+                                           hostname_dns_lookup_callback,
+                                           resolver);
+        nm_clear_g_source(&resolver->timeout_id);
+    }
+
+    switch (resolver->state) {
+    case RESOLVER_WAIT_ADDRESS:
+        if (!resolver->timeout_id)
+            resolver->timeout_id = g_timeout_add(30000, hostname_dns_address_timeout, resolver);
+        NM_SET_OUT(out_wait, TRUE);
+        return NULL;
+    case RESOLVER_IN_PROGRESS:
+        NM_SET_OUT(out_wait, TRUE);
+        return NULL;
+    case RESOLVER_DONE:
+        NM_SET_OUT(out_wait, FALSE);
+        return resolver->hostname;
+    }
+
+    return nm_assert_unreachable_val(NULL);
+}
+
+/*****************************************************************************/
+
+static const char *
+_activation_func_to_string(ActivationHandleFunc func)
+{
+#define FUNC_TO_STRING_CHECK_AND_RETURN(func, f) \
+    G_STMT_START                                 \
+    {                                            \
+        if ((func) == (f))                       \
+            return #f;                           \
+    }                                            \
+    G_STMT_END
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage1_device_prepare);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage2_device_config);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage3_ip_config_start);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage4_ip_config_timeout_4);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage4_ip_config_timeout_6);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage5_ip_config_result_4);
+    FUNC_TO_STRING_CHECK_AND_RETURN(func, activate_stage5_ip_config_result_6);
+    g_return_val_if_reached("unknown");
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDevice *       self = NM_DEVICE(object);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    GVariantBuilder  array_builder;
+
+    switch (prop_id) {
+    case PROP_UDI:
+        /* UDI is (depending on the device type) a path to sysfs and can contain
+         * non-UTF-8.
+         *   ip link add name $'d\xccf\\c' type dummy  */
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->udi, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE));
+        break;
+    case PROP_PATH:
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->path, NM_UTILS_STR_UTF8_SAFE_FLAG_NONE));
+        break;
+    case PROP_IFACE:
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->iface, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL));
+        break;
+    case PROP_IP_IFACE:
+        if (ip_config_valid(priv->state)) {
+            g_value_take_string(
+                value,
+                nm_utils_str_utf8safe_escape_cp(nm_device_get_ip_iface(self),
+                                                NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL));
+        } else
+            g_value_set_string(value, NULL);
+        break;
+    case PROP_IFINDEX:
+        g_value_set_int(value, priv->ifindex);
+        break;
+    case PROP_DRIVER:
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->driver, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL));
+        break;
+    case PROP_DRIVER_VERSION:
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->driver_version,
+                                            NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL));
+        break;
+    case PROP_FIRMWARE_VERSION:
+        g_value_take_string(
+            value,
+            nm_utils_str_utf8safe_escape_cp(priv->firmware_version,
+                                            NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL));
+        break;
+    case PROP_CAPABILITIES:
+        g_value_set_uint(value, (priv->capabilities & ~NM_DEVICE_CAP_INTERNAL_MASK));
+        break;
+    case PROP_IP4_ADDRESS:
+        g_value_set_variant(value, nm_g_variant_singleton_u_0());
+        break;
+    case PROP_CARRIER:
+        g_value_set_boolean(value, priv->carrier);
+        break;
+    case PROP_MTU:
+        g_value_set_uint(value, priv->mtu);
+        break;
+    case PROP_IP4_CONFIG:
+        nm_dbus_utils_g_value_set_object_path(value,
+                                              ip_config_valid(priv->state) ? priv->ip_config_4
+                                                                           : NULL);
+        break;
+    case PROP_DHCP4_CONFIG:
+        nm_dbus_utils_g_value_set_object_path(
+            value,
+            ip_config_valid(priv->state) ? priv->dhcp_data_4.config : NULL);
+        break;
+    case PROP_IP6_CONFIG:
+        nm_dbus_utils_g_value_set_object_path(value,
+                                              ip_config_valid(priv->state) ? priv->ip_config_6
+                                                                           : NULL);
+        break;
+    case PROP_DHCP6_CONFIG:
+        nm_dbus_utils_g_value_set_object_path(
+            value,
+            ip_config_valid(priv->state) ? priv->dhcp_data_6.config : NULL);
+        break;
+    case PROP_STATE:
+        g_value_set_uint(value, priv->state);
+        break;
+    case PROP_STATE_REASON:
+        g_value_take_variant(value, g_variant_new("(uu)", priv->state, priv->state_reason));
+        break;
+    case PROP_ACTIVE_CONNECTION:
+        g_value_set_string(value, nm_dbus_track_obj_path_get(&priv->act_request));
+        break;
+    case PROP_DEVICE_TYPE:
+        g_value_set_uint(value, priv->type);
+        break;
+    case PROP_LINK_TYPE:
+        g_value_set_uint(value, priv->link_type);
+        break;
+    case PROP_MANAGED:
+        /* The managed state exposed on D-Bus only depends on the current device state alone. */
+        g_value_set_boolean(value, nm_device_get_state(self) > NM_DEVICE_STATE_UNMANAGED);
+        break;
+    case PROP_AUTOCONNECT:
+        g_value_set_boolean(
+            value,
+            nm_device_autoconnect_blocked_get(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL) ? FALSE
+                                                                                       : TRUE);
+        break;
+    case PROP_FIRMWARE_MISSING:
+        g_value_set_boolean(value, priv->firmware_missing);
+        break;
+    case PROP_NM_PLUGIN_MISSING:
+        g_value_set_boolean(value, priv->nm_plugin_missing);
+        break;
+    case PROP_TYPE_DESC:
+        g_value_set_string(value, priv->type_desc);
+        break;
+    case PROP_RFKILL_TYPE:
+        g_value_set_uint(value, priv->rfkill_type);
+        break;
+    case PROP_AVAILABLE_CONNECTIONS:
+        nm_dbus_utils_g_value_set_object_path_from_hash(value, priv->available_connections, TRUE);
+        break;
+    case PROP_PHYSICAL_PORT_ID:
+        g_value_set_string(value, priv->physical_port_id);
+        break;
+    case PROP_MASTER:
+        g_value_set_object(value, nm_device_get_master(self));
+        break;
+    case PROP_PARENT:
+        g_value_set_string(value, nm_dbus_track_obj_path_get(&priv->parent_device));
+        break;
+    case PROP_HW_ADDRESS:
+        g_value_set_string(value, priv->hw_addr);
+        break;
+    case PROP_PERM_HW_ADDRESS:
+    {
+        const char *perm_hw_addr;
+        gboolean    perm_hw_addr_is_fake;
+
+        perm_hw_addr = nm_device_get_permanent_hw_address_full(self, FALSE, &perm_hw_addr_is_fake);
+        /* this property is exposed on D-Bus for NMDeviceEthernet and NMDeviceWifi. */
+        g_value_set_string(value, perm_hw_addr && !perm_hw_addr_is_fake ? perm_hw_addr : NULL);
+        break;
+    }
+    case PROP_HAS_PENDING_ACTION:
+        g_value_set_boolean(value, nm_device_has_pending_action(self));
+        break;
+    case PROP_METERED:
+        g_value_set_uint(value, priv->metered);
+        break;
+    case PROP_LLDP_NEIGHBORS:
+        if (priv->lldp_listener)
+            g_value_set_variant(value, nm_lldp_listener_get_neighbors(priv->lldp_listener));
+        else {
+            g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aa{sv}"));
+            g_value_take_variant(value, g_variant_builder_end(&array_builder));
+        }
+        break;
+    case PROP_REAL:
+        g_value_set_boolean(value, nm_device_is_real(self));
+        break;
+    case PROP_SLAVES:
+    {
+        CList *slave_iter;
+        char **slave_list;
+        gsize  i, n;
+
+        n          = c_list_length(&priv->slaves);
+        slave_list = g_new(char *, n + 1);
+        i          = 0;
+        c_list_for_each (slave_iter, &priv->slaves) {
+            SlaveInfo * info = c_list_entry(slave_iter, SlaveInfo, lst_slave);
+            const char *path;
+
+            if (!NM_DEVICE_GET_PRIVATE(info->slave)->is_enslaved)
+                continue;
+            path = nm_dbus_object_get_path(NM_DBUS_OBJECT(info->slave));
+            if (path)
+                slave_list[i++] = g_strdup(path);
+        }
+        nm_assert(i <= n);
+        slave_list[i] = NULL;
+        g_value_take_boxed(value, slave_list);
+        break;
+    }
+    case PROP_STATISTICS_REFRESH_RATE_MS:
+        g_value_set_uint(value, priv->stats.refresh_rate_ms);
+        break;
+    case PROP_STATISTICS_TX_BYTES:
+        g_value_set_uint64(value, priv->stats.tx_bytes);
+        break;
+    case PROP_STATISTICS_RX_BYTES:
+        g_value_set_uint64(value, priv->stats.rx_bytes);
+        break;
+    case PROP_IP4_CONNECTIVITY:
+        g_value_set_uint(value, priv->concheck_x[1].state);
+        break;
+    case PROP_IP6_CONNECTIVITY:
+        g_value_set_uint(value, priv->concheck_x[0].state);
+        break;
+    case PROP_INTERFACE_FLAGS:
+        g_value_set_uint(value, priv->interface_flags);
+        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)
+{
+    NMDevice *       self = (NMDevice *) object;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_UDI:
+        /* construct-only */
+        priv->udi = g_value_dup_string(value);
+        break;
+    case PROP_IFACE:
+        /* construct-only */
+        priv->iface_ = g_value_dup_string(value);
+        break;
+    case PROP_DRIVER:
+        /* construct-only */
+        priv->driver = g_value_dup_string(value);
+        break;
+    case PROP_MANAGED:
+        /* via D-Bus */
+        if (nm_device_is_real(self)) {
+            gboolean            managed;
+            NMDeviceStateReason reason;
+
+            managed = g_value_get_boolean(value);
+            if (managed) {
+                reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED;
+                if (NM_IN_SET_TYPED(NMDeviceSysIfaceState,
+                                    priv->sys_iface_state,
+                                    NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+                                    NM_DEVICE_SYS_IFACE_STATE_REMOVED))
+                    nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_ASSUME);
+            } else {
+                reason = NM_DEVICE_STATE_REASON_REMOVED;
+                nm_device_sys_iface_state_set(self, NM_DEVICE_SYS_IFACE_STATE_REMOVED);
+            }
+            nm_device_set_unmanaged_by_flags(self, NM_UNMANAGED_USER_EXPLICIT, !managed, reason);
+        }
+        break;
+    case PROP_AUTOCONNECT:
+        /* via D-Bus */
+        if (g_value_get_boolean(value))
+            nm_device_autoconnect_blocked_unset(self, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL);
+        else
+            nm_device_autoconnect_blocked_set(self, NM_DEVICE_AUTOCONNECT_BLOCKED_USER);
+        break;
+    case PROP_NM_PLUGIN_MISSING:
+        /* construct-only */
+        priv->nm_plugin_missing = g_value_get_boolean(value);
+        break;
+    case PROP_DEVICE_TYPE:
+        /* construct-only */
+        nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN);
+        priv->type = g_value_get_uint(value);
+        nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN);
+        nm_assert(priv->type <= NM_DEVICE_TYPE_VRF);
+        break;
+    case PROP_LINK_TYPE:
+        /* construct-only */
+        nm_assert(priv->link_type == NM_LINK_TYPE_NONE);
+        priv->link_type = g_value_get_uint(value);
+        break;
+    case PROP_TYPE_DESC:
+        /* construct-only */
+        priv->type_desc = g_value_dup_string(value);
+        break;
+    case PROP_RFKILL_TYPE:
+        /* construct-only */
+        priv->rfkill_type = g_value_get_uint(value);
+        break;
+    case PROP_PERM_HW_ADDRESS:
+        /* construct-only */
+        priv->hw_addr_perm = g_value_dup_string(value);
+        break;
+    case PROP_STATISTICS_REFRESH_RATE_MS:
+        /* via D-Bus */
+        _stats_set_refresh_rate(self, g_value_get_uint(value));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_init(NMDevice *self)
+{
+    NMDevicePrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_DEVICE, NMDevicePrivate);
+
+    self->_priv = priv;
+
+    c_list_init(&priv->concheck_lst_head);
+    c_list_init(&self->devices_lst);
+    c_list_init(&priv->slaves);
+
+    priv->concheck_x[0].state = NM_CONNECTIVITY_UNKNOWN;
+    priv->concheck_x[1].state = NM_CONNECTIVITY_UNKNOWN;
+
+    nm_dbus_track_obj_path_init(&priv->parent_device, G_OBJECT(self), obj_properties[PROP_PARENT]);
+    nm_dbus_track_obj_path_init(&priv->act_request,
+                                G_OBJECT(self),
+                                obj_properties[PROP_ACTIVE_CONNECTION]);
+
+    priv->netns = g_object_ref(NM_NETNS_GET);
+
+    priv->autoconnect_blocked_flags = DEFAULT_AUTOCONNECT ? NM_DEVICE_AUTOCONNECT_BLOCKED_NONE
+                                                          : NM_DEVICE_AUTOCONNECT_BLOCKED_USER;
+
+    priv->auth_retries          = NM_DEVICE_AUTH_RETRIES_UNSET;
+    priv->type                  = NM_DEVICE_TYPE_UNKNOWN;
+    priv->capabilities          = NM_DEVICE_CAP_NM_SUPPORTED;
+    priv->state                 = NM_DEVICE_STATE_UNMANAGED;
+    priv->state_reason          = NM_DEVICE_STATE_REASON_NONE;
+    priv->rfkill_type           = RFKILL_TYPE_UNKNOWN;
+    priv->unmanaged_flags       = NM_UNMANAGED_PLATFORM_INIT;
+    priv->unmanaged_mask        = priv->unmanaged_flags;
+    priv->available_connections = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL);
+    priv->ip6_saved_properties  = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);
+    priv->sys_iface_state_      = NM_DEVICE_SYS_IFACE_STATE_EXTERNAL;
+
+    priv->v4_commit_first_time = TRUE;
+    priv->v6_commit_first_time = TRUE;
+}
+
+static GObject *
+constructor(GType type, guint n_construct_params, GObjectConstructParam *construct_params)
+{
+    GObject *             object;
+    GObjectClass *        klass;
+    NMDevice *            self;
+    NMDevicePrivate *     priv;
+    const NMPlatformLink *pllink;
+
+    klass  = G_OBJECT_CLASS(nm_device_parent_class);
+    object = klass->constructor(type, n_construct_params, construct_params);
+    if (!object)
+        return NULL;
+
+    self = NM_DEVICE(object);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (priv->iface && G_LIKELY(!nm_utils_get_testing())) {
+        pllink = nm_platform_link_get_by_ifname(nm_device_get_platform(self), priv->iface);
+
+        if (pllink && link_type_compatible(self, pllink->type, NULL, NULL)) {
+            _set_ifindex(self, pllink->ifindex, FALSE);
+            priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP);
+        }
+    }
+
+    if (priv->hw_addr_perm) {
+        guint8 buf[NM_UTILS_HWADDR_LEN_MAX];
+        gsize  l;
+
+        if (!_nm_utils_hwaddr_aton(priv->hw_addr_perm, buf, sizeof(buf), &l)) {
+            nm_clear_g_free(&priv->hw_addr_perm);
+            g_return_val_if_reached(object);
+        }
+
+        priv->hw_addr_len_ = l;
+        priv->hw_addr      = nm_utils_hwaddr_ntoa(buf, l);
+        _LOGT(LOGD_DEVICE, "hw-addr: has permanent hw-address '%s'", priv->hw_addr_perm);
+    }
+
+    return object;
+}
+
+static void
+constructed(GObject *object)
+{
+    NMDevice *       self = NM_DEVICE(object);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform *     platform;
+
+    if (NM_DEVICE_GET_CLASS(self)->get_generic_capabilities)
+        priv->capabilities |= NM_DEVICE_GET_CLASS(self)->get_generic_capabilities(self);
+
+    /* Watch for external IP config changes */
+    platform = nm_device_get_platform(self);
+    g_signal_connect(platform,
+                     NM_PLATFORM_SIGNAL_IP4_ADDRESS_CHANGED,
+                     G_CALLBACK(device_ipx_changed),
+                     self);
+    g_signal_connect(platform,
+                     NM_PLATFORM_SIGNAL_IP6_ADDRESS_CHANGED,
+                     G_CALLBACK(device_ipx_changed),
+                     self);
+    g_signal_connect(platform,
+                     NM_PLATFORM_SIGNAL_IP4_ROUTE_CHANGED,
+                     G_CALLBACK(device_ipx_changed),
+                     self);
+    g_signal_connect(platform,
+                     NM_PLATFORM_SIGNAL_IP6_ROUTE_CHANGED,
+                     G_CALLBACK(device_ipx_changed),
+                     self);
+    g_signal_connect(platform, NM_PLATFORM_SIGNAL_LINK_CHANGED, G_CALLBACK(link_changed_cb), self);
+
+    priv->manager  = g_object_ref(NM_MANAGER_GET);
+    priv->settings = g_object_ref(NM_SETTINGS_GET);
+
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_ADDED,
+                     G_CALLBACK(cp_connection_added),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_UPDATED,
+                     G_CALLBACK(cp_connection_updated),
+                     self);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
+                     G_CALLBACK(cp_connection_removed),
+                     self);
+
+    G_OBJECT_CLASS(nm_device_parent_class)->constructed(object);
+
+    _LOGD(LOGD_DEVICE, "constructed (%s)", G_OBJECT_TYPE_NAME(self));
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDevice *                  self = NM_DEVICE(object);
+    NMDevicePrivate *           priv = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform *                platform;
+    NMDeviceConnectivityHandle *con_handle;
+    gs_free_error GError *cancelled_error = NULL;
+
+    _LOGD(LOGD_DEVICE, "disposing");
+
+    nm_assert(c_list_is_empty(&self->devices_lst));
+
+    while ((con_handle = c_list_first_entry(&priv->concheck_lst_head,
+                                            NMDeviceConnectivityHandle,
+                                            concheck_lst))) {
+        if (!cancelled_error)
+            nm_utils_error_set_cancelled(&cancelled_error, FALSE, "NMDevice");
+        concheck_handle_complete(con_handle, cancelled_error);
+    }
+
+    nm_clear_g_cancellable(&priv->deactivating_cancellable);
+
+    nm_device_assume_state_reset(self);
+
+    _parent_set_ifindex(self, 0, FALSE);
+
+    platform = nm_device_get_platform(self);
+    g_signal_handlers_disconnect_by_func(platform, G_CALLBACK(device_ipx_changed), self);
+    g_signal_handlers_disconnect_by_func(platform, G_CALLBACK(link_changed_cb), self);
+
+    arp_cleanup(self);
+
+    nm_clear_g_signal_handler(nm_config_get(), &priv->config_changed_id);
+    nm_clear_g_signal_handler(priv->manager, &priv->ifindex_changed_id);
+
+    dispatcher_cleanup(self);
+
+    nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
+
+    _cleanup_generic_pre(self, CLEANUP_TYPE_KEEP);
+
+    nm_assert(c_list_is_empty(&priv->slaves));
+
+    /* Let the kernel manage IPv6LL again */
+    set_nm_ipv6ll(self, FALSE);
+
+    _cleanup_generic_post(self, CLEANUP_TYPE_KEEP);
+
+    nm_assert(priv->master_ready_id == 0);
+
+    g_hash_table_remove_all(priv->ip6_saved_properties);
+
+    nm_clear_g_source(&priv->recheck_assume_id);
+    nm_clear_g_source(&priv->recheck_available.call_id);
+
+    nm_clear_g_source(&priv->check_delete_unrealized_id);
+
+    nm_clear_g_source(&priv->stats.timeout_id);
+
+    carrier_disconnected_action_cancel(self);
+
+    _set_ifindex(self, 0, FALSE);
+    _set_ifindex(self, 0, TRUE);
+
+    if (priv->settings) {
+        g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_added, self);
+        g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_updated, self);
+        g_signal_handlers_disconnect_by_func(priv->settings, cp_connection_removed, self);
+    }
+
+    available_connections_del_all(self);
+
+    if (nm_clear_g_source(&priv->carrier_wait_id))
+        nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
+
+    _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED);
+
+    nm_clear_g_source(&priv->device_link_changed_id);
+    nm_clear_g_source(&priv->device_ip_link_changed_id);
+
+    if (priv->lldp_listener) {
+        g_signal_handlers_disconnect_by_func(priv->lldp_listener,
+                                             G_CALLBACK(lldp_neighbors_changed),
+                                             self);
+        nm_lldp_listener_stop(priv->lldp_listener);
+        g_clear_object(&priv->lldp_listener);
+    }
+
+    nm_clear_g_source(&priv->concheck_x[0].p_cur_id);
+    nm_clear_g_source(&priv->concheck_x[1].p_cur_id);
+
+    nm_assert(!priv->sriov.pending);
+    if (priv->sriov.next) {
+        nm_g_slice_free(priv->sriov.next);
+        priv->sriov.next = NULL;
+    }
+
+    G_OBJECT_CLASS(nm_device_parent_class)->dispose(object);
+
+    if (nm_clear_g_source(&priv->queued_state.id)) {
+        /* FIXME: we'd expect the queud_state to be already cleared and this statement
+         * not being necessary. Add this check here to hopefully investigate crash
+         * rh#1270247. */
+        g_return_if_reached();
+    }
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDevice *       self = NM_DEVICE(object);
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    _LOGD(LOGD_DEVICE, "finalize(): %s", G_OBJECT_TYPE_NAME(self));
+
+    g_free(priv->hw_addr);
+    g_free(priv->hw_addr_perm);
+    g_free(priv->hw_addr_initial);
+    g_slist_free(priv->pending_actions);
+    g_slist_free_full(priv->dad6_failed_addrs, (GDestroyNotify) nmp_object_unref);
+    nm_clear_g_free(&priv->physical_port_id);
+    g_free(priv->udi);
+    g_free(priv->path);
+    g_free(priv->iface_);
+    g_free(priv->ip_iface_);
+    g_free(priv->driver);
+    g_free(priv->driver_version);
+    g_free(priv->firmware_version);
+    g_free(priv->type_desc);
+    g_free(priv->dhcp_anycast_address);
+    g_free(priv->current_stable_id);
+
+    g_hash_table_unref(priv->ip6_saved_properties);
+    g_hash_table_unref(priv->available_connections);
+
+    nm_dbus_track_obj_path_deinit(&priv->parent_device);
+    nm_dbus_track_obj_path_deinit(&priv->act_request);
+
+    G_OBJECT_CLASS(nm_device_parent_class)->finalize(object);
+
+    /* for testing, NMDeviceTest does not invoke NMDevice::constructed,
+     * and thus @settings might be unset. */
+    nm_g_object_unref(priv->settings);
+    nm_g_object_unref(priv->manager);
+
+    nm_g_object_unref(priv->concheck_mgr);
+
+    g_object_unref(priv->netns);
+}
+
+/*****************************************************************************/
+
+static const GDBusSignalInfo signal_info_state_changed = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
+    "StateChanged",
+    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("new_state", "u"),
+                                      NM_DEFINE_GDBUS_ARG_INFO("old_state", "u"),
+                                      NM_DEFINE_GDBUS_ARG_INFO("reason", "u"), ), );
+
+static const NMDBusInterfaceInfoExtended interface_info_device = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE,
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "Reapply",
+                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"),
+                        NM_DEFINE_GDBUS_ARG_INFO("version_id", "t"),
+                        NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ), ),
+                .handle = impl_device_reapply, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "GetAppliedConnection",
+                    .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ),
+                    .out_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("connection", "a{sa{sv}}"),
+                        NM_DEFINE_GDBUS_ARG_INFO("version_id", "t"), ), ),
+                .handle = impl_device_get_applied_connection, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Disconnect", ),
+                                                .handle = impl_device_disconnect, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Delete", ),
+                                                .handle = impl_device_delete, ), ),
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&signal_info_state_changed, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Udi", "s", NM_DEVICE_UDI),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Path", "s", NM_DEVICE_PATH),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Interface", "s", NM_DEVICE_IFACE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("IpInterface",
+                                                             "s",
+                                                             NM_DEVICE_IP_IFACE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Driver", "s", NM_DEVICE_DRIVER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DriverVersion",
+                                                             "s",
+                                                             NM_DEVICE_DRIVER_VERSION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FirmwareVersion",
+                                                             "s",
+                                                             NM_DEVICE_FIRMWARE_VERSION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Capabilities",
+                                                             "u",
+                                                             NM_DEVICE_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip4Address",
+                                                             "u",
+                                                             NM_DEVICE_IP4_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("State", "u", NM_DEVICE_STATE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("StateReason",
+                                                             "(uu)",
+                                                             NM_DEVICE_STATE_REASON),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ActiveConnection",
+                                                             "o",
+                                                             NM_DEVICE_ACTIVE_CONNECTION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip4Config",
+                                                             "o",
+                                                             NM_DEVICE_IP4_CONFIG),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Dhcp4Config",
+                                                             "o",
+                                                             NM_DEVICE_DHCP4_CONFIG),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ip6Config",
+                                                             "o",
+                                                             NM_DEVICE_IP6_CONFIG),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Dhcp6Config",
+                                                             "o",
+                                                             NM_DEVICE_DHCP6_CONFIG),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE_L("Managed",
+                                                                 "b",
+                                                                 NM_DEVICE_MANAGED,
+                                                                 NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                                                                 NM_AUDIT_OP_DEVICE_MANAGED),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE_L("Autoconnect",
+                                                                 "b",
+                                                                 NM_DEVICE_AUTOCONNECT,
+                                                                 NM_AUTH_PERMISSION_NETWORK_CONTROL,
+                                                                 NM_AUDIT_OP_DEVICE_AUTOCONNECT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("FirmwareMissing",
+                                                             "b",
+                                                             NM_DEVICE_FIRMWARE_MISSING),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("NmPluginMissing",
+                                                             "b",
+                                                             NM_DEVICE_NM_PLUGIN_MISSING),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("DeviceType",
+                                                             "u",
+                                                             NM_DEVICE_DEVICE_TYPE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AvailableConnections",
+                                                             "ao",
+                                                             NM_DEVICE_AVAILABLE_CONNECTIONS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PhysicalPortId",
+                                                             "s",
+                                                             NM_DEVICE_PHYSICAL_PORT_ID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mtu", "u", NM_DEVICE_MTU),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Metered", "u", NM_DEVICE_METERED),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("LldpNeighbors",
+                                                             "aa{sv}",
+                                                             NM_DEVICE_LLDP_NEIGHBORS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Real", "b", NM_DEVICE_REAL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ip4Connectivity",
+                                                           "u",
+                                                           NM_DEVICE_IP4_CONNECTIVITY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ip6Connectivity",
+                                                           "u",
+                                                           NM_DEVICE_IP6_CONNECTIVITY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("InterfaceFlags",
+                                                           "u",
+                                                           NM_DEVICE_INTERFACE_FLAGS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
+                                                           "s",
+                                                           NM_DEVICE_HW_ADDRESS), ), ),
+};
+
+const NMDBusInterfaceInfoExtended nm_interface_info_device_statistics = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_STATISTICS,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE(
+                "RefreshRateMs",
+                "u",
+                NM_DEVICE_STATISTICS_REFRESH_RATE_MS,
+                NM_AUTH_PERMISSION_ENABLE_DISABLE_STATISTICS,
+                NM_AUDIT_OP_STATISTICS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("TxBytes",
+                                                           "t",
+                                                           NM_DEVICE_STATISTICS_TX_BYTES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RxBytes",
+                                                           "t",
+                                                           NM_DEVICE_STATISTICS_RX_BYTES), ), ),
+};
+
+static void
+nm_device_class_init(NMDeviceClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMDevicePrivate));
+
+    dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/Devices");
+    dbus_object_class->interface_infos =
+        NM_DBUS_INTERFACE_INFOS(&interface_info_device, &nm_interface_info_device_statistics);
+
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+    object_class->set_property = set_property;
+    object_class->get_property = get_property;
+    object_class->constructor  = constructor;
+    object_class->constructed  = constructed;
+
+    klass->link_changed = link_changed;
+
+    klass->is_available                 = is_available;
+    klass->act_stage2_config            = act_stage2_config;
+    klass->act_stage3_ip_config_start   = act_stage3_ip_config_start;
+    klass->act_stage4_ip_config_timeout = act_stage4_ip_config_timeout;
+
+    klass->get_type_description          = get_type_description;
+    klass->can_auto_connect              = can_auto_connect;
+    klass->can_update_from_platform_link = can_update_from_platform_link;
+    klass->check_connection_compatible   = check_connection_compatible;
+    klass->check_connection_available    = check_connection_available;
+    klass->can_unmanaged_external_down   = can_unmanaged_external_down;
+    klass->realize_start_notify          = realize_start_notify;
+    klass->unrealize_notify              = unrealize_notify;
+    klass->carrier_changed_notify        = carrier_changed_notify;
+    klass->get_ip_iface_identifier       = get_ip_iface_identifier;
+    klass->unmanaged_on_quit             = unmanaged_on_quit;
+    klass->deactivate_reset_hw_addr      = deactivate_reset_hw_addr;
+    klass->parent_changed_notify         = parent_changed_notify;
+    klass->can_reapply_change            = can_reapply_change;
+    klass->reapply_connection            = reapply_connection;
+    klass->set_platform_mtu              = set_platform_mtu;
+
+    obj_properties[PROP_UDI] =
+        g_param_spec_string(NM_DEVICE_UDI,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_PATH] = g_param_spec_string(NM_DEVICE_PATH,
+                                                    "",
+                                                    "",
+                                                    NULL,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IFACE] =
+        g_param_spec_string(NM_DEVICE_IFACE,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IP_IFACE] = g_param_spec_string(NM_DEVICE_IP_IFACE,
+                                                        "",
+                                                        "",
+                                                        NULL,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DRIVER] =
+        g_param_spec_string(NM_DEVICE_DRIVER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DRIVER_VERSION] =
+        g_param_spec_string(NM_DEVICE_DRIVER_VERSION,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_FIRMWARE_VERSION] =
+        g_param_spec_string(NM_DEVICE_FIRMWARE_VERSION,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_CAP_NONE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_CARRIER] = g_param_spec_boolean(NM_DEVICE_CARRIER,
+                                                        "",
+                                                        "",
+                                                        FALSE,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_MTU]     = g_param_spec_uint(NM_DEVICE_MTU,
+                                                 "",
+                                                 "",
+                                                 0,
+                                                 G_MAXUINT32,
+                                                 1500,
+                                                 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IP4_ADDRESS] =
+        g_param_spec_variant(NM_DEVICE_IP4_ADDRESS,
+                             "",
+                             "",
+                             G_VARIANT_TYPE_UINT32,
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IP4_CONFIG] =
+        g_param_spec_string(NM_DEVICE_IP4_CONFIG,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DHCP4_CONFIG] =
+        g_param_spec_string(NM_DEVICE_DHCP4_CONFIG,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IP6_CONFIG] =
+        g_param_spec_string(NM_DEVICE_IP6_CONFIG,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DHCP6_CONFIG] =
+        g_param_spec_string(NM_DEVICE_DHCP6_CONFIG,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_STATE] = g_param_spec_uint(NM_DEVICE_STATE,
+                                                   "",
+                                                   "",
+                                                   0,
+                                                   G_MAXUINT32,
+                                                   NM_DEVICE_STATE_UNKNOWN,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_STATE_REASON] =
+        g_param_spec_variant(NM_DEVICE_STATE_REASON,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("(uu)"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_ACTIVE_CONNECTION] =
+        g_param_spec_string(NM_DEVICE_ACTIVE_CONNECTION,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_DEVICE_TYPE] =
+        g_param_spec_uint(NM_DEVICE_DEVICE_TYPE,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_TYPE_UNKNOWN,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_LINK_TYPE] =
+        g_param_spec_uint(NM_DEVICE_LINK_TYPE,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_LINK_TYPE_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_MANAGED]     = g_param_spec_boolean(NM_DEVICE_MANAGED,
+                                                        "",
+                                                        "",
+                                                        FALSE,
+                                                        G_PARAM_READWRITE | /* via D-Bus */
+                                                            G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_AUTOCONNECT] = g_param_spec_boolean(NM_DEVICE_AUTOCONNECT,
+                                                            "",
+                                                            "",
+                                                            DEFAULT_AUTOCONNECT,
+                                                            G_PARAM_READWRITE | /* via D-Bus */
+                                                                G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_FIRMWARE_MISSING] =
+        g_param_spec_boolean(NM_DEVICE_FIRMWARE_MISSING,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_NM_PLUGIN_MISSING] =
+        g_param_spec_boolean(NM_DEVICE_NM_PLUGIN_MISSING,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_TYPE_DESC] =
+        g_param_spec_string(NM_DEVICE_TYPE_DESC,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_RFKILL_TYPE] =
+        g_param_spec_uint(NM_DEVICE_RFKILL_TYPE,
+                          "",
+                          "",
+                          RFKILL_TYPE_WLAN,
+                          RFKILL_TYPE_MAX,
+                          RFKILL_TYPE_UNKNOWN,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IFINDEX] = g_param_spec_int(NM_DEVICE_IFINDEX,
+                                                    "",
+                                                    "",
+                                                    0,
+                                                    G_MAXINT,
+                                                    0,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_AVAILABLE_CONNECTIONS] =
+        g_param_spec_boxed(NM_DEVICE_AVAILABLE_CONNECTIONS,
+                           "",
+                           "",
+                           G_TYPE_STRV,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_PHYSICAL_PORT_ID] =
+        g_param_spec_string(NM_DEVICE_PHYSICAL_PORT_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_MASTER] = g_param_spec_object(NM_DEVICE_MASTER,
+                                                      "",
+                                                      "",
+                                                      NM_TYPE_DEVICE,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_PARENT] = g_param_spec_string(NM_DEVICE_PARENT,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_HW_ADDRESS] =
+        g_param_spec_string(NM_DEVICE_HW_ADDRESS,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_PERM_HW_ADDRESS] =
+        g_param_spec_string(NM_DEVICE_PERM_HW_ADDRESS,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_HAS_PENDING_ACTION] =
+        g_param_spec_boolean(NM_DEVICE_HAS_PENDING_ACTION,
+                             "",
+                             "",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_METERED] = g_param_spec_uint(NM_DEVICE_METERED,
+                                                     "",
+                                                     "",
+                                                     0,
+                                                     G_MAXUINT32,
+                                                     NM_METERED_UNKNOWN,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_LLDP_NEIGHBORS] =
+        g_param_spec_variant(NM_DEVICE_LLDP_NEIGHBORS,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("aa{sv}"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_REAL]   = g_param_spec_boolean(NM_DEVICE_REAL,
+                                                     "",
+                                                     "",
+                                                     FALSE,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_SLAVES] = g_param_spec_boxed(NM_DEVICE_SLAVES,
+                                                     "",
+                                                     "",
+                                                     G_TYPE_STRV,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_STATISTICS_REFRESH_RATE_MS] =
+        g_param_spec_uint(NM_DEVICE_STATISTICS_REFRESH_RATE_MS,
+                          "",
+                          "",
+                          0,
+                          UINT32_MAX,
+                          0,
+                          G_PARAM_READWRITE | /* via D-Bus */
+                              G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_STATISTICS_TX_BYTES] =
+        g_param_spec_uint64(NM_DEVICE_STATISTICS_TX_BYTES,
+                            "",
+                            "",
+                            0,
+                            UINT64_MAX,
+                            0,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_STATISTICS_RX_BYTES] =
+        g_param_spec_uint64(NM_DEVICE_STATISTICS_RX_BYTES,
+                            "",
+                            "",
+                            0,
+                            UINT64_MAX,
+                            0,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_IP4_CONNECTIVITY] =
+        g_param_spec_uint(NM_DEVICE_IP4_CONNECTIVITY,
+                          "",
+                          "",
+                          NM_CONNECTIVITY_UNKNOWN,
+                          NM_CONNECTIVITY_FULL,
+                          NM_CONNECTIVITY_UNKNOWN,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_IP6_CONNECTIVITY] =
+        g_param_spec_uint(NM_DEVICE_IP6_CONNECTIVITY,
+                          "",
+                          "",
+                          NM_CONNECTIVITY_UNKNOWN,
+                          NM_CONNECTIVITY_FULL,
+                          NM_CONNECTIVITY_UNKNOWN,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+    obj_properties[PROP_INTERFACE_FLAGS] =
+        g_param_spec_uint(NM_DEVICE_INTERFACE_FLAGS,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          0,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[STATE_CHANGED] = g_signal_new(NM_DEVICE_STATE_CHANGED,
+                                          G_OBJECT_CLASS_TYPE(object_class),
+                                          G_SIGNAL_RUN_LAST,
+                                          G_STRUCT_OFFSET(NMDeviceClass, state_changed),
+                                          NULL,
+                                          NULL,
+                                          NULL,
+                                          G_TYPE_NONE,
+                                          3,
+                                          G_TYPE_UINT,
+                                          G_TYPE_UINT,
+                                          G_TYPE_UINT);
+
+    signals[AUTOCONNECT_ALLOWED] = g_signal_new(NM_DEVICE_AUTOCONNECT_ALLOWED,
+                                                G_OBJECT_CLASS_TYPE(object_class),
+                                                G_SIGNAL_RUN_LAST,
+                                                0,
+                                                autoconnect_allowed_accumulator,
+                                                NULL,
+                                                NULL,
+                                                G_TYPE_BOOLEAN,
+                                                0);
+
+    signals[IP4_CONFIG_CHANGED] = g_signal_new(NM_DEVICE_IP4_CONFIG_CHANGED,
+                                               G_OBJECT_CLASS_TYPE(object_class),
+                                               G_SIGNAL_RUN_FIRST,
+                                               0,
+                                               NULL,
+                                               NULL,
+                                               NULL,
+                                               G_TYPE_NONE,
+                                               2,
+                                               G_TYPE_OBJECT,
+                                               G_TYPE_OBJECT);
+
+    signals[IP6_CONFIG_CHANGED] = g_signal_new(NM_DEVICE_IP6_CONFIG_CHANGED,
+                                               G_OBJECT_CLASS_TYPE(object_class),
+                                               G_SIGNAL_RUN_FIRST,
+                                               0,
+                                               NULL,
+                                               NULL,
+                                               NULL,
+                                               G_TYPE_NONE,
+                                               2,
+                                               G_TYPE_OBJECT,
+                                               G_TYPE_OBJECT);
+
+    signals[IP6_PREFIX_DELEGATED] = g_signal_new(NM_DEVICE_IP6_PREFIX_DELEGATED,
+                                                 G_OBJECT_CLASS_TYPE(object_class),
+                                                 G_SIGNAL_RUN_FIRST,
+                                                 0,
+                                                 NULL,
+                                                 NULL,
+                                                 NULL,
+                                                 G_TYPE_NONE,
+                                                 1,
+                                                 G_TYPE_POINTER);
+
+    signals[IP6_SUBNET_NEEDED] = g_signal_new(NM_DEVICE_IP6_SUBNET_NEEDED,
+                                              G_OBJECT_CLASS_TYPE(object_class),
+                                              G_SIGNAL_RUN_FIRST,
+                                              0,
+                                              NULL,
+                                              NULL,
+                                              NULL,
+                                              G_TYPE_NONE,
+                                              0);
+
+    signals[REMOVED] = g_signal_new(NM_DEVICE_REMOVED,
+                                    G_OBJECT_CLASS_TYPE(object_class),
+                                    G_SIGNAL_RUN_FIRST,
+                                    0,
+                                    NULL,
+                                    NULL,
+                                    NULL,
+                                    G_TYPE_NONE,
+                                    0);
+
+    signals[RECHECK_AUTO_ACTIVATE] = g_signal_new(NM_DEVICE_RECHECK_AUTO_ACTIVATE,
+                                                  G_OBJECT_CLASS_TYPE(object_class),
+                                                  G_SIGNAL_RUN_FIRST,
+                                                  0,
+                                                  NULL,
+                                                  NULL,
+                                                  NULL,
+                                                  G_TYPE_NONE,
+                                                  0);
+
+    signals[RECHECK_ASSUME] = g_signal_new(NM_DEVICE_RECHECK_ASSUME,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_FIRST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           0);
+
+    signals[DNS_LOOKUP_DONE] = g_signal_new(NM_DEVICE_DNS_LOOKUP_DONE,
+                                            G_OBJECT_CLASS_TYPE(object_class),
+                                            G_SIGNAL_RUN_FIRST,
+                                            0,
+                                            NULL,
+                                            NULL,
+                                            NULL,
+                                            G_TYPE_NONE,
+                                            0);
+}
+
+/* Connection defaults from plugins */
+NM_CON_DEFAULT_NOP("cdma.mtu");
+NM_CON_DEFAULT_NOP("gsm.mtu");
+NM_CON_DEFAULT_NOP("wifi.ap-isolation");
+NM_CON_DEFAULT_NOP("wifi.powersave");
+NM_CON_DEFAULT_NOP("wifi.wake-on-wlan");
+NM_CON_DEFAULT_NOP("wifi-sec.pmf");
+NM_CON_DEFAULT_NOP("wifi-sec.fils");
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
new file mode 100644
index 00000000..72777d0a
--- /dev/null
+++ b/src/core/devices/nm-device.h
@@ -0,0 +1,874 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2017 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_H__
+#define __NETWORKMANAGER_DEVICE_H__
+
+#include <netinet/in.h>
+
+#include "nm-setting-connection.h"
+#include "nm-dbus-object.h"
+#include "nm-dbus-interface.h"
+#include "nm-connection.h"
+#include "nm-rfkill-manager.h"
+#include "NetworkManagerUtils.h"
+
+typedef enum _nm_packed {
+    NM_DEVICE_SYS_IFACE_STATE_EXTERNAL,
+    NM_DEVICE_SYS_IFACE_STATE_ASSUME,
+    NM_DEVICE_SYS_IFACE_STATE_MANAGED,
+
+    /* the REMOVED state applies when the device is manually set to unmanaged
+     * or the link was externally removed. In both cases, we move the device
+     * to UNMANAGED state, without touching the link -- be it, because the link
+     * is already gone or because we want to release it (give it up).
+     */
+    NM_DEVICE_SYS_IFACE_STATE_REMOVED,
+} NMDeviceSysIfaceState;
+
+typedef enum {
+    NM_DEVICE_MTU_SOURCE_NONE,
+    NM_DEVICE_MTU_SOURCE_PARENT,
+    NM_DEVICE_MTU_SOURCE_IP_CONFIG,
+    NM_DEVICE_MTU_SOURCE_CONNECTION,
+} NMDeviceMtuSource;
+
+static inline NMDeviceStateReason
+nm_device_state_reason_check(NMDeviceStateReason reason)
+{
+    /* the device-state-reason serves mostly informational purpose during a state
+     * change. In some cases however, decisions are made based on the reason.
+     * I tend to think that interpreting the state reason to derive some behaviors
+     * is confusing, because the cause and effect are so far apart.
+     *
+     * This function is here to mark source that inspects the reason to make
+     * a decision -- contrary to places that set the reason. Thus, by grepping
+     * for nm_device_state_reason_check() you can find the "effect" to a certain
+     * reason.
+     */
+    return reason;
+}
+
+#define NM_PENDING_ACTION_AUTOACTIVATE           "autoactivate"
+#define NM_PENDING_ACTION_IN_STATE_CHANGE        "in-state-change"
+#define NM_PENDING_ACTION_RECHECK_AVAILABLE      "recheck-available"
+#define NM_PENDING_ACTION_CARRIER_WAIT           "carrier-wait"
+#define NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT "waiting-for-supplicant"
+#define NM_PENDING_ACTION_WIFI_SCAN              "wifi-scan"
+#define NM_PENDING_ACTION_WAITING_FOR_COMPANION  "waiting-for-companion"
+#define NM_PENDING_ACTION_LINK_INIT              "link-init"
+
+#define NM_PENDING_ACTIONPREFIX_QUEUED_STATE_CHANGE "queued-state-change-"
+#define NM_PENDING_ACTIONPREFIX_ACTIVATION          "activation-"
+
+/* Properties */
+#define NM_DEVICE_UDI                   "udi"
+#define NM_DEVICE_PATH                  "path"
+#define NM_DEVICE_IFACE                 "interface"
+#define NM_DEVICE_IP_IFACE              "ip-interface"
+#define NM_DEVICE_DRIVER                "driver"
+#define NM_DEVICE_DRIVER_VERSION        "driver-version"
+#define NM_DEVICE_FIRMWARE_VERSION      "firmware-version"
+#define NM_DEVICE_CAPABILITIES          "capabilities"
+#define NM_DEVICE_CARRIER               "carrier"
+#define NM_DEVICE_IP4_ADDRESS           "ip4-address"
+#define NM_DEVICE_IP4_CONFIG            "ip4-config"
+#define NM_DEVICE_DHCP4_CONFIG          "dhcp4-config"
+#define NM_DEVICE_IP6_CONFIG            "ip6-config"
+#define NM_DEVICE_DHCP6_CONFIG          "dhcp6-config"
+#define NM_DEVICE_STATE                 "state"
+#define NM_DEVICE_STATE_REASON          "state-reason"
+#define NM_DEVICE_ACTIVE_CONNECTION     "active-connection"
+#define NM_DEVICE_DEVICE_TYPE           "device-type" /* ugh */
+#define NM_DEVICE_LINK_TYPE             "link-type"
+#define NM_DEVICE_MANAGED               "managed"
+#define NM_DEVICE_AUTOCONNECT           "autoconnect"
+#define NM_DEVICE_FIRMWARE_MISSING      "firmware-missing"
+#define NM_DEVICE_NM_PLUGIN_MISSING     "nm-plugin-missing"
+#define NM_DEVICE_AVAILABLE_CONNECTIONS "available-connections"
+#define NM_DEVICE_PHYSICAL_PORT_ID      "physical-port-id"
+#define NM_DEVICE_MTU                   "mtu"
+#define NM_DEVICE_HW_ADDRESS            "hw-address"
+
+/* "perm-hw-address" is exposed on D-Bus both for NMDeviceEthernet
+ * and NMDeviceWifi. */
+#define NM_DEVICE_PERM_HW_ADDRESS "perm-hw-address"
+
+#define NM_DEVICE_METERED        "metered"
+#define NM_DEVICE_LLDP_NEIGHBORS "lldp-neighbors"
+#define NM_DEVICE_REAL           "real"
+
+/* "parent" is exposed on D-Bus by subclasses like NMDeviceIPTunnel */
+#define NM_DEVICE_PARENT "parent"
+
+/* the "slaves" property is internal in the parent class, but exposed
+ * by the derived classes NMDeviceBond, NMDeviceBridge, NMDeviceTeam,
+ * NMDeviceOvsBridge and NMDeviceOvsPort. */
+#define NM_DEVICE_SLAVES "slaves" /* partially internal */
+
+#define NM_DEVICE_TYPE_DESC          "type-desc" /* Internal only */
+#define NM_DEVICE_RFKILL_TYPE        "rfkill-type" /* Internal only */
+#define NM_DEVICE_IFINDEX            "ifindex" /* Internal only */
+#define NM_DEVICE_MASTER             "master" /* Internal only */
+#define NM_DEVICE_HAS_PENDING_ACTION "has-pending-action" /* Internal only */
+
+/* Internal signals */
+#define NM_DEVICE_DNS_LOOKUP_DONE       "dns-lookup-done"
+#define NM_DEVICE_IP4_CONFIG_CHANGED    "ip4-config-changed"
+#define NM_DEVICE_IP6_CONFIG_CHANGED    "ip6-config-changed"
+#define NM_DEVICE_IP6_PREFIX_DELEGATED  "ip6-prefix-delegated"
+#define NM_DEVICE_IP6_SUBNET_NEEDED     "ip6-subnet-needed"
+#define NM_DEVICE_REMOVED               "removed"
+#define NM_DEVICE_RECHECK_AUTO_ACTIVATE "recheck-auto-activate"
+#define NM_DEVICE_RECHECK_ASSUME        "recheck-assume"
+#define NM_DEVICE_STATE_CHANGED         "state-changed"
+#define NM_DEVICE_LINK_INITIALIZED      "link-initialized"
+#define NM_DEVICE_AUTOCONNECT_ALLOWED   "autoconnect-allowed"
+
+#define NM_DEVICE_STATISTICS_REFRESH_RATE_MS "refresh-rate-ms"
+#define NM_DEVICE_STATISTICS_TX_BYTES        "tx-bytes"
+#define NM_DEVICE_STATISTICS_RX_BYTES        "rx-bytes"
+
+#define NM_DEVICE_IP4_CONNECTIVITY "ip4-connectivity"
+#define NM_DEVICE_IP6_CONNECTIVITY "ip6-connectivity"
+#define NM_DEVICE_INTERFACE_FLAGS  "interface-flags"
+
+#define NM_TYPE_DEVICE            (nm_device_get_type())
+#define NM_DEVICE(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE, NMDevice))
+#define NM_DEVICE_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE, NMDeviceClass))
+#define NM_IS_DEVICE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE))
+#define NM_IS_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE))
+#define NM_DEVICE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE, NMDeviceClass))
+
+typedef enum NMActStageReturn NMActStageReturn;
+
+/* These flags affect whether a connection is considered available on a device
+ * (check_connection_available()). The flags should have the meaning of relaxing
+ * a condition, so that adding a flag might make a connection available that would
+ * not be available otherwise. Adding a flag should never make a connection
+ * not available if it would be available otherwise. */
+typedef enum { /*< skip >*/
+               NM_DEVICE_CHECK_CON_AVAILABLE_NONE = 0,
+
+               /* since NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST is a collection of flags with more fine grained
+     * parts, this flag in general indicates that this is a user-request. */
+               _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST = (1L << 0),
+
+               /* we also consider devices which have no carrier but are still waiting for the driver
+     * to detect carrier. Usually, such devices are not yet available, however for a user-request
+     * they are. They might fail later if carrier doesn't come. */
+               _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER = (1L << 1),
+
+               /* usually, a profile is only available if the Wi-Fi AP is in range. For an
+     * explicit user request, we also consider profiles for APs that are not (yet)
+     * visible. */
+               _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP = (1L << 2),
+
+               /* a device can be marked as unmanaged for various reasons. Some of these reasons
+     * are authoritative, others not. Non-authoritative reasons can be overruled by
+     * `nmcli device set $DEVICE managed yes`. Also, for an explicit user activation
+     * request we may want to consider the device as managed. This flag makes devices
+     * that are unmanaged appear available. */
+               _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED = (1L << 3),
+
+               /* a collection of flags, that are commonly set for an explicit user-request. */
+               NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST =
+                   _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST
+                   | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER
+                   | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP
+                   | _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_OVERRULE_UNMANAGED,
+
+               NM_DEVICE_CHECK_CON_AVAILABLE_ALL = (1L << 4) - 1,
+} NMDeviceCheckConAvailableFlags;
+
+struct _NMDevicePrivate;
+
+struct _NMDevice {
+    NMDBusObject             parent;
+    struct _NMDevicePrivate *_priv;
+    CList                    devices_lst;
+};
+
+/* The flags have an relaxing meaning, that means, specifying more flags, can make
+ * a device appear more available. It can never make a device less available. */
+typedef enum { /*< skip >*/
+               NM_DEVICE_CHECK_DEV_AVAILABLE_NONE = 0,
+
+               /* the device is considered available, even if it has no carrier.
+     *
+     * For various device types (software devices) we ignore carrier based
+     * on the type. So, for them, this flag has no effect anyway. */
+               _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER = (1L << 0),
+
+               NM_DEVICE_CHECK_DEV_AVAILABLE_FOR_USER_REQUEST =
+                   _NM_DEVICE_CHECK_DEV_AVAILABLE_IGNORE_CARRIER,
+
+               NM_DEVICE_CHECK_DEV_AVAILABLE_ALL = (1L << 1) - 1,
+} NMDeviceCheckDevAvailableFlags;
+
+typedef void (*NMDeviceDeactivateCallback)(NMDevice *self, GError *error, gpointer user_data);
+
+typedef struct _NMDeviceClass {
+    NMDBusObjectClass parent;
+
+    struct _NMDeviceClass *default_type_description_klass;
+    const char *           default_type_description;
+
+    const char *connection_type_supported;
+
+    /* most device types, can only handle profiles of a particular type. This
+     * is the connection.type setting, as checked by nm_device_check_connection_compatible() */
+    const char *connection_type_check_compatible;
+
+    const NMLinkType *link_types;
+
+    /* if the device MTU is set based on parent's one, this specifies
+     * a delta in the MTU allowed value due the encapsulation overhead */
+    guint16 mtu_parent_delta;
+
+    /* Whether the device type is a master-type. This depends purely on the
+     * type (NMDeviceClass), not the actual device instance. */
+    bool is_master : 1;
+
+    /* Force setting the MTU actually means first setting the MTU
+     * to (desired_MTU-1) and then setting the desired_MTU
+     * so that kernel actually applies the MTU, otherwise
+     * kernel will ignore the request if the link's MTU is the
+     * same as the desired one.
+     *
+     * This is just a workaround made for bridges (ATM) that employ
+     * a auto-MTU adjust mechanism if no MTU is manually set.
+     */
+    bool mtu_force_set : 1;
+
+    /* Control whether to call stage1 and stage2 callbacks also for assuming
+     * a device or for external activations. In this case, the callback must
+     * take care not to touch the device's configuration. */
+    bool act_stage1_prepare_also_for_external_or_assume : 1;
+    bool act_stage2_config_also_for_external_or_assume : 1;
+
+    bool act_stage1_prepare_set_hwaddr_ethernet : 1;
+
+    bool can_reapply_change_ovs_external_ids : 1;
+
+    void (*state_changed)(NMDevice *          device,
+                          NMDeviceState       new_state,
+                          NMDeviceState       old_state,
+                          NMDeviceStateReason reason);
+
+    void (*link_changed)(NMDevice *self, const NMPlatformLink *pllink);
+
+    /**
+     * create_and_realize():
+     * @self: the #NMDevice
+     * @connection: the #NMConnection being activated
+     * @parent: the parent #NMDevice, if any
+     * @out_plink: on success, a backing kernel network device if one exists.
+     *   The returned pointer is owned by platform and only valid until the
+     *   next platform operation.
+     * @error: location to store error, or %NULL
+     *
+     * Create any backing resources (kernel devices, etc) required for this
+     * device to activate @connection.  If the device is backed by a kernel
+     * network device, that device should be returned in @out_plink after
+     * being created.
+     *
+     * Returns: %TRUE on success, %FALSE on error
+     */
+    gboolean (*create_and_realize)(NMDevice *             self,
+                                   NMConnection *         connection,
+                                   NMDevice *             parent,
+                                   const NMPlatformLink **out_plink,
+                                   GError **              error);
+
+    /**
+     * realize_start_notify():
+     * @self: the #NMDevice
+     * @pllink: the #NMPlatformLink if backed by a kernel netdevice
+     *
+     * Hook for derived classes to be notfied during realize_start_setup()
+     * and perform additional setup.
+     *
+     * The default implementation of NMDevice calls link_changed().
+     */
+    void (*realize_start_notify)(NMDevice *self, const NMPlatformLink *pllink);
+
+    /**
+     * unrealize():
+     * @self: the #NMDevice
+     *
+     * Remove the device backing resources.
+     */
+    gboolean (*unrealize)(NMDevice *self, GError **error);
+
+    /**
+     * unrealize_notify():
+     * @self: the #NMDevice
+     *
+     * Hook for derived classes to clear any properties that depend on backing resources
+     * (kernel devices, etc). This is called by nm_device_unrealize() during unrealization.
+     */
+    void (*unrealize_notify)(NMDevice *self);
+
+    /* Hardware state (IFF_UP) */
+    gboolean (*can_unmanaged_external_down)(NMDevice *self);
+
+    /* Carrier state (IFF_LOWER_UP) */
+    void (*carrier_changed_notify)(NMDevice *, gboolean carrier);
+
+    gboolean (*get_ip_iface_identifier)(NMDevice *self, NMUtilsIPv6IfaceId *out_iid);
+
+    NMDeviceCapabilities (*get_generic_capabilities)(NMDevice *self);
+
+    gboolean (*is_available)(NMDevice *self, NMDeviceCheckDevAvailableFlags flags);
+
+    gboolean (*get_enabled)(NMDevice *self);
+
+    void (*set_enabled)(NMDevice *self, gboolean enabled);
+
+    /* let the subclass return additional NMPlatformRoutingRule (in form of NMPObject
+     * pointers) that shall be added to the rules provided by this device.
+     * The returned GPtrArray will be g_ptr_array_unref()'ed. The subclass may or
+     * may not keep an additional reference and return this array again and again. */
+    GPtrArray *(*get_extra_rules)(NMDevice *self);
+
+    /* allow derived classes to override the result of nm_device_autoconnect_allowed().
+     * If the value changes, the class should call nm_device_emit_recheck_auto_activate(),
+     * which emits NM_DEVICE_RECHECK_AUTO_ACTIVATE signal. */
+    gboolean (*get_autoconnect_allowed)(NMDevice *self);
+
+    gboolean (*can_auto_connect)(NMDevice *            self,
+                                 NMSettingsConnection *sett_conn,
+                                 char **               specific_object);
+
+    guint32 (*get_configured_mtu)(NMDevice *         self,
+                                  NMDeviceMtuSource *out_source,
+                                  gboolean *         out_force);
+
+    /* allow the subclass to overwrite the routing table. This is mainly useful
+     * to change from partial mode (route-table=0) to full-sync mode (route-table=254). */
+    guint32 (*coerce_route_table)(NMDevice *self,
+                                  int       addr_family,
+                                  guint32   route_table,
+                                  gboolean  is_user_config);
+
+    const char *(*get_auto_ip_config_method)(NMDevice *self, int addr_family);
+
+    /* Checks whether the connection is compatible with the device using
+     * only the devices type and characteristics.  Does not use any live
+     * network information like Wi-Fi scan lists etc.
+     */
+    gboolean (*check_connection_compatible)(NMDevice *    self,
+                                            NMConnection *connection,
+                                            GError **     error);
+
+    /* Checks whether the connection is likely available to be activated,
+     * including any live network information like scan lists.  The connection
+     * is checked against the object defined by @specific_object, if given.
+     * Returns TRUE if the connection is available; FALSE if not.
+     *
+     * The passed @flags affect whether a connection is considered
+     * available or not. Adding more flags, means the connection is
+     * *more* available.
+     *
+     * Specifying @specific_object can only reduce the availability of a connection.
+     */
+    gboolean (*check_connection_available)(NMDevice *                     self,
+                                           NMConnection *                 connection,
+                                           NMDeviceCheckConAvailableFlags flags,
+                                           const char *                   specific_object,
+                                           GError **                      error);
+
+    gboolean (*complete_connection)(NMDevice *           self,
+                                    NMConnection *       connection,
+                                    const char *         specific_object,
+                                    NMConnection *const *existing_connections,
+                                    GError **            error);
+
+    NMActStageReturn (*act_stage1_prepare)(NMDevice *self, NMDeviceStateReason *out_failure_reason);
+    NMActStageReturn (*act_stage2_config)(NMDevice *self, NMDeviceStateReason *out_failure_reason);
+    NMActStageReturn (*act_stage3_ip_config_start)(NMDevice *           self,
+                                                   int                  addr_family,
+                                                   gpointer *           out_config,
+                                                   NMDeviceStateReason *out_failure_reason);
+    NMActStageReturn (*act_stage4_ip_config_timeout)(NMDevice *           self,
+                                                     int                  addr_family,
+                                                     NMDeviceStateReason *out_failure_reason);
+
+    void (*ip4_config_pre_commit)(NMDevice *self, NMIP4Config *config);
+
+    /* Async deactivating (in the DEACTIVATING phase) */
+    void (*deactivate_async)(NMDevice *                 self,
+                             GCancellable *             cancellable,
+                             NMDeviceDeactivateCallback callback,
+                             gpointer                   user_data);
+
+    void (*deactivate_reset_hw_addr)(NMDevice *self);
+
+    /* Sync deactivating (in the DISCONNECTED phase) */
+    void (*deactivate)(NMDevice *self);
+
+    const char *(*get_type_description)(NMDevice *self);
+
+    const char *(*get_s390_subchannels)(NMDevice *self);
+
+    /* Update the connection with currently configured L2 settings */
+    void (*update_connection)(NMDevice *device, NMConnection *connection);
+
+    gboolean (*master_update_slave_connection)(NMDevice *    self,
+                                               NMDevice *    slave,
+                                               NMConnection *connection,
+                                               GError **     error);
+
+    gboolean (*enslave_slave)(NMDevice *    self,
+                              NMDevice *    slave,
+                              NMConnection *connection,
+                              gboolean      configure);
+
+    void (*release_slave)(NMDevice *self, NMDevice *slave, gboolean configure);
+
+    void (*parent_changed_notify)(NMDevice *self,
+                                  int       old_ifindex,
+                                  NMDevice *old_parent,
+                                  int       new_ifindex,
+                                  NMDevice *new_parent);
+
+    gboolean (*owns_iface)(NMDevice *self, const char *iface);
+
+    NMConnection *(*new_default_connection)(NMDevice *self);
+
+    gboolean (*unmanaged_on_quit)(NMDevice *self);
+
+    gboolean (*can_reapply_change)(NMDevice *  self,
+                                   const char *setting_name,
+                                   NMSetting * s_old,
+                                   NMSetting * s_new,
+                                   GHashTable *diffs,
+                                   GError **   error);
+
+    void (*reapply_connection)(NMDevice *self, NMConnection *con_old, NMConnection *con_new);
+
+    guint32 (*get_dhcp_timeout_for_device)(NMDevice *self, int addr_family);
+
+    gboolean (*get_guessed_metered)(NMDevice *self);
+
+    gboolean (*can_update_from_platform_link)(NMDevice *self, const NMPlatformLink *plink);
+
+    gboolean (*set_platform_mtu)(NMDevice *self, guint32 mtu);
+
+} NMDeviceClass;
+
+GType nm_device_get_type(void);
+
+struct _NMDedupMultiIndex *nm_device_get_multi_index(NMDevice *self);
+NMNetns *                  nm_device_get_netns(NMDevice *self);
+NMPlatform *               nm_device_get_platform(NMDevice *self);
+
+const char *nm_device_get_udi(NMDevice *dev);
+const char *nm_device_get_iface(NMDevice *dev);
+
+static inline const char *
+_nm_device_get_iface(NMDevice *device)
+{
+    /* like nm_device_get_iface(), but gracefully accept NULL without
+     * asserting. */
+    return device ? nm_device_get_iface(device) : NULL;
+}
+
+int          nm_device_get_ifindex(NMDevice *dev);
+gboolean     nm_device_is_software(NMDevice *dev);
+gboolean     nm_device_is_real(NMDevice *dev);
+const char * nm_device_get_ip_iface(NMDevice *dev);
+const char * nm_device_get_ip_iface_from_platform(NMDevice *dev);
+int          nm_device_get_ip_ifindex(const NMDevice *dev);
+const char * nm_device_get_driver(NMDevice *dev);
+const char * nm_device_get_driver_version(NMDevice *dev);
+const char * nm_device_get_type_desc(NMDevice *dev);
+const char * nm_device_get_type_description(NMDevice *dev);
+NMDeviceType nm_device_get_device_type(NMDevice *dev);
+NMLinkType   nm_device_get_link_type(NMDevice *dev);
+NMMetered    nm_device_get_metered(NMDevice *dev);
+
+guint32 nm_device_get_route_table(NMDevice *self, int addr_family);
+guint32 nm_device_get_route_metric(NMDevice *dev, int addr_family);
+
+guint32 nm_device_get_route_metric_default(NMDeviceType device_type);
+
+const char *nm_device_get_hw_address(NMDevice *dev);
+const char *nm_device_get_permanent_hw_address(NMDevice *self);
+const char *nm_device_get_permanent_hw_address_full(NMDevice *self,
+                                                    gboolean  force_freeze,
+                                                    gboolean *out_is_fake);
+const char *nm_device_get_initial_hw_address(NMDevice *dev);
+
+NMProxyConfig *nm_device_get_proxy_config(NMDevice *dev);
+
+NMDhcpConfig *nm_device_get_dhcp_config(NMDevice *dev, int addr_family);
+NMIP4Config * nm_device_get_ip4_config(NMDevice *dev);
+void          nm_device_replace_vpn4_config(NMDevice *dev, NMIP4Config *old, NMIP4Config *config);
+
+NMIP6Config *nm_device_get_ip6_config(NMDevice *dev);
+void         nm_device_replace_vpn6_config(NMDevice *dev, NMIP6Config *old, NMIP6Config *config);
+
+void nm_device_capture_initial_config(NMDevice *dev);
+
+int       nm_device_parent_get_ifindex(NMDevice *dev);
+NMDevice *nm_device_parent_get_device(NMDevice *dev);
+void      nm_device_parent_set_ifindex(NMDevice *self, int parent_ifindex);
+gboolean  nm_device_parent_notify_changed(NMDevice *self,
+                                          NMDevice *change_candidate,
+                                          gboolean  device_removed);
+
+const char *nm_device_parent_find_for_connection(NMDevice *  self,
+                                                 const char *current_setting_parent);
+
+/* Master */
+gboolean nm_device_is_master(NMDevice *dev);
+
+/* Slave */
+NMDevice *nm_device_get_master(NMDevice *dev);
+
+NMActRequest *         nm_device_get_act_request(NMDevice *dev);
+NMSettingsConnection * nm_device_get_settings_connection(NMDevice *dev);
+NMConnection *         nm_device_get_settings_connection_get_connection(NMDevice *self);
+NMConnection *         nm_device_get_applied_connection(NMDevice *dev);
+gboolean               nm_device_has_unmodified_applied_connection(NMDevice *            self,
+                                                                   NMSettingCompareFlags compare_flags);
+NMActivationStateFlags nm_device_get_activation_state_flags(NMDevice *self);
+
+gpointer /* (NMSetting *) */ nm_device_get_applied_setting(NMDevice *dev, GType setting_type);
+
+void nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config);
+
+gboolean nm_device_ignore_carrier_by_default(NMDevice *self);
+
+gboolean nm_device_is_available(NMDevice *dev, NMDeviceCheckDevAvailableFlags flags);
+gboolean nm_device_has_carrier(NMDevice *dev);
+
+NMConnection *nm_device_generate_connection(NMDevice *self,
+                                            NMDevice *master,
+                                            gboolean *out_maybe_later,
+                                            GError ** error);
+
+gboolean nm_device_master_update_slave_connection(NMDevice *    master,
+                                                  NMDevice *    slave,
+                                                  NMConnection *connection,
+                                                  GError **     error);
+
+gboolean
+nm_device_can_auto_connect(NMDevice *self, NMSettingsConnection *sett_conn, char **specific_object);
+
+gboolean nm_device_complete_connection(NMDevice *           device,
+                                       NMConnection *       connection,
+                                       const char *         specific_object,
+                                       NMConnection *const *existing_connections,
+                                       GError **            error);
+
+gboolean
+nm_device_check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error);
+
+gboolean nm_device_check_slave_connection_compatible(NMDevice *device, NMConnection *connection);
+
+gboolean nm_device_unmanage_on_quit(NMDevice *self);
+
+gboolean nm_device_spec_match_list(NMDevice *device, const GSList *specs);
+int      nm_device_spec_match_list_full(NMDevice *self, const GSList *specs, int no_match_value);
+
+gboolean nm_device_is_activating(NMDevice *dev);
+gboolean nm_device_autoconnect_allowed(NMDevice *self);
+
+NMDeviceState nm_device_get_state(NMDevice *device);
+
+gboolean nm_device_get_enabled(NMDevice *device);
+
+void nm_device_set_enabled(NMDevice *device, gboolean enabled);
+
+RfKillType nm_device_get_rfkill_type(NMDevice *device);
+
+/* IPv6 prefix delegation */
+
+void nm_device_request_ip6_prefixes(NMDevice *self, int needed_prefixes);
+
+gboolean nm_device_needs_ip6_subnet(NMDevice *self);
+
+void nm_device_use_ip6_subnet(NMDevice *self, const NMPlatformIP6Address *subnet);
+
+void nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device);
+
+/**
+ * NMUnmanagedFlags:
+ * @NM_UNMANAGED_NONE: placeholder value
+ * @NM_UNMANAGED_SLEEPING: %TRUE when unmanaged because NM is sleeping.
+ * @NM_UNMANAGED_QUITTING: %TRUE when unmanaged because NM is shutting down.
+ * @NM_UNMANAGED_PARENT: %TRUE when unmanaged due to parent device being unmanaged
+ * @NM_UNMANAGED_BY_TYPE: %TRUE for unmanaging device by type, like loopback.
+ * @NM_UNMANAGED_PLATFORM_INIT: %TRUE when unmanaged because platform link not
+ *   yet initialized. Unrealized device are also unmanaged for this reason.
+ * @NM_UNMANAGED_USER_EXPLICIT: %TRUE when unmanaged by explicit user decision
+ *   (e.g. via a D-Bus command)
+ * @NM_UNMANAGED_USER_SETTINGS: %TRUE when unmanaged by user decision via
+ *   the settings plugin (for example keyfile.unmanaged-devices or ifcfg-rh's
+ *   NM_CONTROLLED=no). Although this is user-configuration (provided from
+ *   the settings plugins, such as NM_CONTROLLED=no in ifcfg-rh), it cannot
+ *   be overruled and is authoritative. That is because users may depend on
+ *   dropping a ifcfg-rh file to ensure the device is unmanaged.
+ * @NM_UNMANAGED_USER_CONF: %TRUE when unmanaged by user decision via
+ *   the NetworkManager.conf ("unmanaged" in the [device] section).
+ *   Contray to @NM_UNMANAGED_USER_SETTINGS, this can be overwritten via
+ *   D-Bus.
+ * @NM_UNMANAGED_BY_DEFAULT: %TRUE for certain device types where we unmanage
+ *   them by default
+ * @NM_UNMANAGED_USER_UDEV: %TRUE when unmanaged by user decision (via UDev rule)
+ * @NM_UNMANAGED_EXTERNAL_DOWN: %TRUE when unmanaged because !IFF_UP and not created by NM
+ * @NM_UNMANAGED_IS_SLAVE: indicates that the device is enslaved. Note that
+ *   setting the NM_UNMANAGED_IS_SLAVE to %TRUE makes no sense, this flag has only
+ *   meaning to set a slave device as managed if the parent is managed too.
+ */
+typedef enum { /*< skip >*/
+               NM_UNMANAGED_NONE = 0,
+
+               /* these flags are authoritative. If one of them is set,
+     * the device cannot be managed. */
+               NM_UNMANAGED_SLEEPING      = (1LL << 0),
+               NM_UNMANAGED_QUITTING      = (1LL << 1),
+               NM_UNMANAGED_PARENT        = (1LL << 2),
+               NM_UNMANAGED_BY_TYPE       = (1LL << 3),
+               NM_UNMANAGED_PLATFORM_INIT = (1LL << 4),
+               NM_UNMANAGED_USER_EXPLICIT = (1LL << 5),
+               NM_UNMANAGED_USER_SETTINGS = (1LL << 6),
+
+               /* These flags can be non-effective and be overwritten
+     * by other flags. */
+               NM_UNMANAGED_BY_DEFAULT    = (1LL << 8),
+               NM_UNMANAGED_USER_CONF     = (1LL << 9),
+               NM_UNMANAGED_USER_UDEV     = (1LL << 10),
+               NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 11),
+               NM_UNMANAGED_IS_SLAVE      = (1LL << 12),
+
+} NMUnmanagedFlags;
+
+typedef enum {
+    NM_UNMAN_FLAG_OP_SET_MANAGED   = FALSE,
+    NM_UNMAN_FLAG_OP_SET_UNMANAGED = TRUE,
+    NM_UNMAN_FLAG_OP_FORGET        = 2,
+} NMUnmanFlagOp;
+
+const char *nm_unmanaged_flags2str(NMUnmanagedFlags flags, char *buf, gsize len);
+
+gboolean         nm_device_get_managed(NMDevice *device, gboolean for_user_request);
+NMUnmanagedFlags nm_device_get_unmanaged_mask(NMDevice *device, NMUnmanagedFlags flag);
+NMUnmanagedFlags nm_device_get_unmanaged_flags(NMDevice *device, NMUnmanagedFlags flag);
+void nm_device_set_unmanaged_flags(NMDevice *device, NMUnmanagedFlags flags, NMUnmanFlagOp set_op);
+void nm_device_set_unmanaged_by_flags(NMDevice *          device,
+                                      NMUnmanagedFlags    flags,
+                                      NMUnmanFlagOp       set_op,
+                                      NMDeviceStateReason reason);
+void nm_device_set_unmanaged_by_flags_queue(NMDevice *          self,
+                                            NMUnmanagedFlags    flags,
+                                            NMUnmanFlagOp       set_op,
+                                            NMDeviceStateReason reason);
+void nm_device_set_unmanaged_by_user_settings(NMDevice *self);
+void nm_device_set_unmanaged_by_user_udev(NMDevice *self);
+void nm_device_set_unmanaged_by_user_conf(NMDevice *self);
+void nm_device_set_unmanaged_by_quitting(NMDevice *device);
+
+gboolean nm_device_check_unrealized_device_managed(NMDevice *self);
+
+gboolean nm_device_is_nm_owned(NMDevice *device);
+
+gboolean nm_device_has_capability(NMDevice *self, NMDeviceCapabilities caps);
+
+/*****************************************************************************/
+
+void nm_device_assume_state_get(NMDevice *   self,
+                                gboolean *   out_assume_state_guess_assume,
+                                const char **out_assume_state_connection_uuid);
+void nm_device_assume_state_reset(NMDevice *self);
+
+/*****************************************************************************/
+
+gboolean nm_device_realize_start(NMDevice *            device,
+                                 const NMPlatformLink *plink,
+                                 gboolean              assume_state_guess_assume,
+                                 const char *          assume_state_connection_uuid,
+                                 gboolean              set_nm_owned,
+                                 NMUnmanFlagOp         unmanaged_user_explicit,
+                                 gboolean *            out_compatible,
+                                 GError **             error);
+void     nm_device_realize_finish(NMDevice *self, const NMPlatformLink *plink);
+gboolean nm_device_create_and_realize(NMDevice *    self,
+                                      NMConnection *connection,
+                                      NMDevice *    parent,
+                                      GError **     error);
+gboolean nm_device_unrealize(NMDevice *device, gboolean remove_resources, GError **error);
+
+void nm_device_update_from_platform_link(NMDevice *self, const NMPlatformLink *plink);
+
+typedef enum {
+    NM_DEVICE_AUTOCONNECT_BLOCKED_NONE = 0,
+
+    NM_DEVICE_AUTOCONNECT_BLOCKED_USER = (1LL << 0),
+
+    NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN         = (1LL << 1),
+    NM_DEVICE_AUTOCONNECT_BLOCKED_MANUAL_DISCONNECT = (1LL << 2),
+    NM_DEVICE_AUTOCONNECT_BLOCKED_SIM_MISSING       = (1LL << 3),
+    NM_DEVICE_AUTOCONNECT_BLOCKED_INIT_FAILED       = (1LL << 4),
+
+    _NM_DEVICE_AUTOCONNECT_BLOCKED_LAST,
+
+    NM_DEVICE_AUTOCONNECT_BLOCKED_ALL = (((_NM_DEVICE_AUTOCONNECT_BLOCKED_LAST - 1) << 1) - 1),
+
+    NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL =
+        NM_DEVICE_AUTOCONNECT_BLOCKED_ALL & ~NM_DEVICE_AUTOCONNECT_BLOCKED_USER,
+} NMDeviceAutoconnectBlockedFlags;
+
+NMDeviceAutoconnectBlockedFlags
+nm_device_autoconnect_blocked_get(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask);
+
+void nm_device_autoconnect_blocked_set_full(NMDevice *                      device,
+                                            NMDeviceAutoconnectBlockedFlags mask,
+                                            NMDeviceAutoconnectBlockedFlags values);
+
+static inline void
+nm_device_autoconnect_blocked_set(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask)
+{
+    nm_device_autoconnect_blocked_set_full(device, mask, mask);
+}
+
+static inline void
+nm_device_autoconnect_blocked_unset(NMDevice *device, NMDeviceAutoconnectBlockedFlags mask)
+{
+    nm_device_autoconnect_blocked_set_full(device, mask, NM_DEVICE_AUTOCONNECT_BLOCKED_NONE);
+}
+
+void nm_device_emit_recheck_auto_activate(NMDevice *device);
+
+NMDeviceSysIfaceState nm_device_sys_iface_state_get(NMDevice *device);
+
+gboolean nm_device_sys_iface_state_is_external(NMDevice *self);
+gboolean nm_device_sys_iface_state_is_external_or_assume(NMDevice *self);
+
+void nm_device_sys_iface_state_set(NMDevice *device, NMDeviceSysIfaceState sys_iface_state);
+
+void nm_device_state_changed(NMDevice *device, NMDeviceState state, NMDeviceStateReason reason);
+
+void nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason);
+
+gboolean nm_device_get_firmware_missing(NMDevice *self);
+
+void nm_device_disconnect_active_connection(NMActiveConnection *          active,
+                                            NMDeviceStateReason           device_reason,
+                                            NMActiveConnectionStateReason active_reason);
+
+void nm_device_queue_activation(NMDevice *device, NMActRequest *req);
+
+gboolean nm_device_supports_vlans(NMDevice *device);
+
+gboolean
+nm_device_add_pending_action(NMDevice *device, const char *action, gboolean assert_not_yet_pending);
+gboolean
+nm_device_remove_pending_action(NMDevice *device, const char *action, gboolean assert_is_pending);
+const char *nm_device_has_pending_action_reason(NMDevice *device);
+
+static inline gboolean
+nm_device_has_pending_action(NMDevice *device)
+{
+    return !!nm_device_has_pending_action_reason(device);
+}
+
+NMSettingsConnection *
+nm_device_get_best_connection(NMDevice *device, const char *specific_object, GError **error);
+
+gboolean nm_device_check_connection_available(NMDevice *                     device,
+                                              NMConnection *                 connection,
+                                              NMDeviceCheckConAvailableFlags flags,
+                                              const char *                   specific_object,
+                                              GError **                      error);
+
+void nm_device_notify_availability_maybe_changed(NMDevice *self);
+
+gboolean nm_device_owns_iface(NMDevice *device, const char *iface);
+
+NMConnection *nm_device_new_default_connection(NMDevice *self);
+
+const NMPObject *nm_device_get_best_default_route(NMDevice *self, int addr_family);
+
+void nm_device_spawn_iface_helper(NMDevice *self);
+
+gboolean nm_device_reapply(NMDevice *self, NMConnection *connection, GError **error);
+void     nm_device_reapply_settings_immediately(NMDevice *self);
+
+void nm_device_update_firewall_zone(NMDevice *self);
+void nm_device_update_metered(NMDevice *self);
+void nm_device_reactivate_ip_config(NMDevice *         device,
+                                    int                addr_family,
+                                    NMSettingIPConfig *s_ip_old,
+                                    NMSettingIPConfig *s_ip_new);
+
+gboolean nm_device_update_hw_address(NMDevice *self);
+void     nm_device_update_initial_hw_address(NMDevice *self);
+void     nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_freeze);
+void     nm_device_update_dynamic_ip_setup(NMDevice *self);
+guint    nm_device_get_supplicant_timeout(NMDevice *self);
+
+gboolean nm_device_auth_retries_try_next(NMDevice *self);
+
+gboolean nm_device_hw_addr_get_cloned(NMDevice *    self,
+                                      NMConnection *connection,
+                                      gboolean      is_wifi,
+                                      char **       hwaddr,
+                                      gboolean *    preserve,
+                                      GError **     error);
+
+typedef struct _NMDeviceConnectivityHandle NMDeviceConnectivityHandle;
+
+typedef void (*NMDeviceConnectivityCallback)(NMDevice *                  self,
+                                             NMDeviceConnectivityHandle *handle,
+                                             NMConnectivityState         state,
+                                             GError *                    error,
+                                             gpointer                    user_data);
+
+void nm_device_check_connectivity_update_interval(NMDevice *self);
+
+NMDeviceConnectivityHandle *nm_device_check_connectivity(NMDevice *                   self,
+                                                         int                          addr_family,
+                                                         NMDeviceConnectivityCallback callback,
+                                                         gpointer                     user_data);
+
+void nm_device_check_connectivity_cancel(NMDeviceConnectivityHandle *handle);
+
+NMConnectivityState nm_device_get_connectivity_state(NMDevice *self, int addr_family);
+
+typedef struct _NMBtVTableNetworkServer NMBtVTableNetworkServer;
+
+typedef void (*NMBtVTableRegisterCallback)(GError *error, gpointer user_data);
+
+struct _NMBtVTableNetworkServer {
+    gboolean (*is_available)(const NMBtVTableNetworkServer *vtable,
+                             const char *                   addr,
+                             NMDevice *                     device_accept_busy);
+
+    gboolean (*register_bridge)(const NMBtVTableNetworkServer *vtable,
+                                const char *                   addr,
+                                NMDevice *                     device,
+                                GCancellable *                 cancellable,
+                                NMBtVTableRegisterCallback     callback,
+                                gpointer                       callback_user_data,
+                                GError **                      error);
+    gboolean (*unregister_bridge)(const NMBtVTableNetworkServer *vtable, NMDevice *device);
+};
+
+const char *nm_device_state_to_str(NMDeviceState state);
+const char *nm_device_state_reason_to_str(NMDeviceStateReason reason);
+
+gboolean nm_device_is_vpn(NMDevice *self);
+
+const char *
+nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean *out_pending);
+
+void nm_device_clear_dns_lookup_data(NMDevice *self);
+
+#endif /* __NETWORKMANAGER_DEVICE_H__ */
diff --git a/src/core/devices/nm-lldp-listener.c b/src/core/devices/nm-lldp-listener.c
new file mode 100644
index 00000000..c60fb3ad
--- /dev/null
+++ b/src/core/devices/nm-lldp-listener.c
@@ -0,0 +1,1122 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-lldp-listener.h"
+
+#include <net/ethernet.h>
+
+#include "nm-std-aux/unaligned.h"
+#include "platform/nm-platform.h"
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-utils.h"
+
+#include "systemd/nm-sd.h"
+
+#define MAX_NEIGHBORS            128
+#define MIN_UPDATE_INTERVAL_NSEC (2 * NM_UTILS_NSEC_PER_SEC)
+
+#define LLDP_MAC_NEAREST_BRIDGE \
+    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e}}))
+#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE \
+    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x03}}))
+#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE \
+    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x00}}))
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMLldpListener, PROP_NEIGHBORS, );
+
+typedef struct {
+    sd_lldp *   lldp_handle;
+    GHashTable *lldp_neighbors;
+    GVariant *  variant;
+
+    /* the timestamp in nsec until which we delay updates. */
+    gint64 ratelimit_next_nsec;
+    guint  ratelimit_id;
+
+    int ifindex;
+} NMLldpListenerPrivate;
+
+struct _NMLldpListener {
+    GObject               parent;
+    NMLldpListenerPrivate _priv;
+};
+
+struct _NMLldpListenerClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMLldpListener, nm_lldp_listener, G_TYPE_OBJECT)
+
+#define NM_LLDP_LISTENER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMLldpListener, NM_IS_LLDP_LISTENER)
+
+/*****************************************************************************/
+
+typedef struct {
+    GVariant *        variant;
+    sd_lldp_neighbor *neighbor_sd;
+    char *            chassis_id;
+    char *            port_id;
+    guint8            chassis_id_type;
+    guint8            port_id_type;
+} LldpNeighbor;
+
+/*****************************************************************************/
+
+#define _NMLOG_PREFIX_NAME "lldp"
+#define _NMLOG_DOMAIN      LOGD_DEVICE
+#define _NMLOG(level, ...)                                                                      \
+    G_STMT_START                                                                                \
+    {                                                                                           \
+        const NMLogLevel _level = (level);                                                      \
+                                                                                                \
+        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                        \
+            char _sbuf[64];                                                                     \
+            int  _ifindex = (self) ? NM_LLDP_LISTENER_GET_PRIVATE(self)->ifindex : 0;           \
+                                                                                                \
+            _nm_log(_level,                                                                     \
+                    _NMLOG_DOMAIN,                                                              \
+                    0,                                                                          \
+                    _ifindex > 0 ? nm_platform_link_get_name(NM_PLATFORM_GET, _ifindex) : NULL, \
+                    NULL,                                                                       \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
+                    _NMLOG_PREFIX_NAME,                                                         \
+                    ((_ifindex > 0) ? nm_sprintf_buf(_sbuf, "[%p,%d]", (self), _ifindex)        \
+                                    : ((self) ? nm_sprintf_buf(_sbuf, "[%p]", (self)) : ""))    \
+                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                                     \
+        }                                                                                       \
+    }                                                                                           \
+    G_STMT_END
+
+#define LOG_NEIGH_FMT "CHASSIS=%u/%s PORT=%u/%s"
+#define LOG_NEIGH_ARG(neigh) \
+    (neigh)->chassis_id_type, (neigh)->chassis_id, (neigh)->port_id_type, (neigh)->port_id
+
+/*****************************************************************************/
+
+static void
+lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *out_raw_len)
+{
+    gconstpointer raw_data = NULL;
+    gsize         raw_len  = 0;
+    int           r;
+
+    nm_assert(neigh);
+
+    r = sd_lldp_neighbor_get_raw(neigh->neighbor_sd, &raw_data, &raw_len);
+
+    nm_assert(r >= 0);
+    nm_assert(raw_data);
+    nm_assert(raw_len > 0);
+
+    *out_raw_data = raw_data;
+    *out_raw_len  = raw_len;
+}
+
+static gboolean
+lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd,
+                     guint8 *                 out_chassis_id_type,
+                     const guint8 **          out_chassis_id,
+                     gsize *                  out_chassis_id_len,
+                     guint8 *                 out_port_id_type,
+                     const guint8 **          out_port_id,
+                     gsize *                  out_port_id_len)
+{
+    int r;
+
+    r = sd_lldp_neighbor_get_chassis_id(neighbor_sd,
+                                        out_chassis_id_type,
+                                        (gconstpointer *) out_chassis_id,
+                                        out_chassis_id_len);
+    if (r < 0)
+        return FALSE;
+
+    r = sd_lldp_neighbor_get_port_id(neighbor_sd,
+                                     out_port_id_type,
+                                     (gconstpointer *) out_port_id,
+                                     out_port_id_len);
+    if (r < 0)
+        return FALSE;
+
+    return TRUE;
+}
+
+static guint
+lldp_neighbor_id_hash(gconstpointer ptr)
+{
+    const LldpNeighbor *neigh = ptr;
+    guint8              chassis_id_type;
+    guint8              port_id_type;
+    const guint8 *      chassis_id;
+    const guint8 *      port_id;
+    gsize               chassis_id_len;
+    gsize               port_id_len;
+    NMHashState         h;
+
+    if (!lldp_neighbor_id_get(neigh->neighbor_sd,
+                              &chassis_id_type,
+                              &chassis_id,
+                              &chassis_id_len,
+                              &port_id_type,
+                              &port_id,
+                              &port_id_len)) {
+        nm_assert_not_reached();
+        return 0;
+    }
+
+    nm_hash_init(&h, 23423423u);
+    nm_hash_update_vals(&h, chassis_id_len, port_id_len, chassis_id_type, port_id_type);
+    nm_hash_update(&h, chassis_id, chassis_id_len);
+    nm_hash_update(&h, port_id, port_id_len);
+    return nm_hash_complete(&h);
+}
+
+static int
+lldp_neighbor_id_cmp(const LldpNeighbor *a, const LldpNeighbor *b)
+{
+    guint8        a_chassis_id_type;
+    guint8        b_chassis_id_type;
+    guint8        a_port_id_type;
+    guint8        b_port_id_type;
+    const guint8 *a_chassis_id;
+    const guint8 *b_chassis_id;
+    const guint8 *a_port_id;
+    const guint8 *b_port_id;
+    gsize         a_chassis_id_len;
+    gsize         b_chassis_id_len;
+    gsize         a_port_id_len;
+    gsize         b_port_id_len;
+
+    NM_CMP_SELF(a, b);
+
+    if (!lldp_neighbor_id_get(a->neighbor_sd,
+                              &a_chassis_id_type,
+                              &a_chassis_id,
+                              &a_chassis_id_len,
+                              &a_port_id_type,
+                              &a_port_id,
+                              &a_port_id_len)) {
+        nm_assert_not_reached();
+        return FALSE;
+    }
+
+    if (!lldp_neighbor_id_get(b->neighbor_sd,
+                              &b_chassis_id_type,
+                              &b_chassis_id,
+                              &b_chassis_id_len,
+                              &b_port_id_type,
+                              &b_port_id,
+                              &b_port_id_len)) {
+        nm_assert_not_reached();
+        return FALSE;
+    }
+
+    NM_CMP_DIRECT(a_chassis_id_type, b_chassis_id_type);
+    NM_CMP_DIRECT(a_port_id_type, b_port_id_type);
+    NM_CMP_DIRECT(a_chassis_id_len, b_chassis_id_len);
+    NM_CMP_DIRECT(a_port_id_len, b_port_id_len);
+    NM_CMP_DIRECT_MEMCMP(a_chassis_id, b_chassis_id, a_chassis_id_len);
+    NM_CMP_DIRECT_MEMCMP(a_port_id, b_port_id, a_port_id_len);
+    return 0;
+}
+
+static int
+lldp_neighbor_id_cmp_p(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+    return lldp_neighbor_id_cmp(*((const LldpNeighbor *const *) a),
+                                *((const LldpNeighbor *const *) b));
+}
+
+static gboolean
+lldp_neighbor_id_equal(gconstpointer a, gconstpointer b)
+{
+    return lldp_neighbor_id_cmp(a, b) == 0;
+}
+
+static void
+lldp_neighbor_free(LldpNeighbor *neighbor)
+{
+    if (!neighbor)
+        return;
+
+    g_free(neighbor->chassis_id);
+    g_free(neighbor->port_id);
+    nm_g_variant_unref(neighbor->variant);
+    sd_lldp_neighbor_unref(neighbor->neighbor_sd);
+    nm_g_slice_free(neighbor);
+}
+
+static void
+lldp_neighbor_freep(LldpNeighbor **ptr)
+{
+    lldp_neighbor_free(*ptr);
+}
+
+static gboolean
+lldp_neighbor_equal(LldpNeighbor *a, LldpNeighbor *b)
+{
+    const guint8 *raw_data_a;
+    const guint8 *raw_data_b;
+    gsize         raw_len_a;
+    gsize         raw_len_b;
+
+    if (a->neighbor_sd == b->neighbor_sd)
+        return TRUE;
+
+    lldp_neighbor_get_raw(a, &raw_data_a, &raw_len_a);
+    lldp_neighbor_get_raw(b, &raw_data_b, &raw_len_b);
+    return raw_len_a == raw_len_b && (memcmp(raw_data_a, raw_data_b, raw_len_a) == 0);
+}
+
+static GVariant *
+parse_management_address_tlv(const uint8_t *data, gsize len)
+{
+    GVariantBuilder builder;
+    gsize           addr_len;
+    const guint8 *  v_object_id_arr;
+    gsize           v_object_id_len;
+    const guint8 *  v_address_arr;
+    gsize           v_address_len;
+    guint32         v_interface_number;
+    guint32         v_interface_number_subtype;
+    guint32         v_address_subtype;
+
+    /* 802.1AB-2009 - Figure 8-11
+     *
+     * - TLV type / length        (2 bytes)
+     * - address string length    (1 byte)
+     * - address subtype          (1 byte)
+     * - address                  (1 to 31 bytes)
+     * - interface number subtype (1 byte)
+     * - interface number         (4 bytes)
+     * - OID string length        (1 byte)
+     * - OID                      (0 to 128 bytes)
+     */
+
+    if (len < 11)
+        return NULL;
+
+    nm_assert((data[0] >> 1) == SD_LLDP_TYPE_MGMT_ADDRESS);
+    nm_assert((((data[0] & 1) << 8) + data[1]) + 2 == len);
+
+    data += 2;
+    len -= 2;
+    addr_len = *data; /* length of (address subtype + address) */
+
+    if (addr_len < 2 || addr_len > 32)
+        return NULL;
+    if (len < (1          /* address stringth length */
+               + addr_len /* address subtype + address */
+               + 5        /* interface */
+               + 1))      /* oid */
+        return NULL;
+
+    data++;
+    len--;
+    v_address_subtype = *data;
+    v_address_arr     = &data[1];
+    v_address_len     = addr_len - 1;
+
+    data += addr_len;
+    len -= addr_len;
+    v_interface_number_subtype = *data;
+
+    data++;
+    len--;
+    v_interface_number = unaligned_read_be32(data);
+
+    data += 4;
+    len -= 4;
+    v_object_id_len = *data;
+    if (len < (1 + v_object_id_len))
+        return NULL;
+    data++;
+    v_object_id_arr = data;
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+    nm_g_variant_builder_add_sv_uint32(&builder, "address-subtype", v_address_subtype);
+    nm_g_variant_builder_add_sv_bytearray(&builder, "address", v_address_arr, v_address_len);
+    nm_g_variant_builder_add_sv_uint32(&builder,
+                                       "interface-number-subtype",
+                                       v_interface_number_subtype);
+    nm_g_variant_builder_add_sv_uint32(&builder, "interface-number", v_interface_number);
+    if (v_object_id_len > 0)
+        nm_g_variant_builder_add_sv_bytearray(&builder,
+                                              "object-id",
+                                              v_object_id_arr,
+                                              v_object_id_len);
+    return g_variant_builder_end(&builder);
+}
+
+static char *
+format_network_address(const guint8 *data, gsize sz)
+{
+    NMIPAddr a;
+    int      family;
+
+    if (sz == 5 && data[0] == 1 /* LLDP_MGMT_ADDR_IP4 */) {
+        memcpy(&a, &data[1], sizeof(a.addr4));
+        family = AF_INET;
+    } else if (sz == 17 && data[0] == 2 /* LLDP_MGMT_ADDR_IP6 */) {
+        memcpy(&a, &data[1], sizeof(a.addr6));
+        family = AF_INET6;
+    } else
+        return NULL;
+
+    return nm_utils_inet_ntop_dup(family, &a);
+}
+
+static const char *
+format_string(const guint8 *data, gsize len, gboolean allow_trim, char **out_to_free)
+{
+    gboolean is_null_terminated = FALSE;
+
+    nm_assert(out_to_free && !*out_to_free);
+
+    if (allow_trim) {
+        while (len > 0 && data[len - 1] == '\0') {
+            is_null_terminated = TRUE;
+            len--;
+        }
+    }
+
+    if (len == 0)
+        return NULL;
+
+    if (memchr(data, len, '\0'))
+        return NULL;
+
+    return nm_utils_buf_utf8safe_escape(data,
+                                        is_null_terminated ? -1 : (gssize) len,
+                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
+                                            | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII,
+                                        out_to_free);
+}
+
+static char *
+format_string_cp(const guint8 *data, gsize len, gboolean allow_trim)
+{
+    char *      s_free = NULL;
+    const char *s;
+
+    s = format_string(data, len, allow_trim, &s_free);
+    nm_assert(!s_free || s == s_free);
+    return s ? (s_free ?: g_strdup(s)) : NULL;
+}
+
+static LldpNeighbor *
+lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
+{
+    LldpNeighbor *neigh;
+    guint8        chassis_id_type;
+    guint8        port_id_type;
+    const guint8 *chassis_id;
+    const guint8 *port_id;
+    gsize         chassis_id_len;
+    gsize         port_id_len;
+    gs_free char *s_chassis_id = NULL;
+    gs_free char *s_port_id    = NULL;
+
+    if (!lldp_neighbor_id_get(neighbor_sd,
+                              &chassis_id_type,
+                              &chassis_id,
+                              &chassis_id_len,
+                              &port_id_type,
+                              &port_id,
+                              &port_id_len))
+        return NULL;
+
+    switch (chassis_id_type) {
+    case SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
+    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
+    case SD_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
+    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
+    case SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED:
+        s_chassis_id = format_string_cp(chassis_id, chassis_id_len, FALSE);
+        break;
+    case SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS:
+        s_chassis_id = nm_utils_hwaddr_ntoa(chassis_id, chassis_id_len);
+        break;
+    case SD_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
+        s_chassis_id = format_network_address(chassis_id, chassis_id_len);
+        break;
+    }
+    if (!s_chassis_id) {
+        /* Invalid/unsupported chassis_id? Expose as hex string. This format is not stable, and
+         * in the future we may add a better string representation for these case (thus
+         * changing the API). */
+        s_chassis_id = nm_utils_bin2hexstr_full(chassis_id, chassis_id_len, '\0', FALSE, NULL);
+    }
+
+    switch (port_id_type) {
+    case SD_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
+    case SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
+    case SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
+    case SD_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED:
+        s_port_id = format_string_cp(port_id, port_id_len, FALSE);
+        break;
+    case SD_LLDP_PORT_SUBTYPE_MAC_ADDRESS:
+        s_port_id = nm_utils_hwaddr_ntoa(port_id, port_id_len);
+        break;
+    case SD_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
+        s_port_id = format_network_address(port_id, port_id_len);
+        break;
+    }
+    if (!s_port_id) {
+        /* Invalid/unsupported port_id? Expose as hex string. This format is not stable, and
+         * in the future we may add a better string representation for these case (thus
+         * changing the API). */
+        s_port_id = nm_utils_bin2hexstr_full(port_id, port_id_len, '\0', FALSE, NULL);
+    }
+
+    neigh  = g_slice_new(LldpNeighbor);
+    *neigh = (LldpNeighbor){
+        .neighbor_sd     = sd_lldp_neighbor_ref(neighbor_sd),
+        .chassis_id_type = chassis_id_type,
+        .chassis_id      = g_steal_pointer(&s_chassis_id),
+        .port_id_type    = port_id_type,
+        .port_id         = g_steal_pointer(&s_port_id),
+    };
+    return neigh;
+}
+
+static GVariant *
+lldp_neighbor_to_variant(LldpNeighbor *neigh)
+{
+    struct ether_addr destination_address;
+    GVariantBuilder   builder;
+    const char *      str;
+    const guint8 *    raw_data;
+    gsize             raw_len;
+    uint16_t          u16;
+    uint8_t *         data8;
+    gsize             len;
+    int               r;
+
+    if (neigh->variant)
+        return neigh->variant;
+
+    lldp_neighbor_get_raw(neigh, &raw_data, &raw_len);
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
+
+    nm_g_variant_builder_add_sv_bytearray(&builder, NM_LLDP_ATTR_RAW, raw_data, raw_len);
+    nm_g_variant_builder_add_sv_uint32(&builder,
+                                       NM_LLDP_ATTR_CHASSIS_ID_TYPE,
+                                       neigh->chassis_id_type);
+    nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_CHASSIS_ID, neigh->chassis_id);
+    nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_PORT_ID_TYPE, neigh->port_id_type);
+    nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_ID, neigh->port_id);
+
+    r = sd_lldp_neighbor_get_destination_address(neigh->neighbor_sd, &destination_address);
+    if (r < 0)
+        str = NULL;
+    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE))
+        str = NM_LLDP_DEST_NEAREST_BRIDGE;
+    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_NON_TPMR_BRIDGE))
+        str = NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE;
+    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_CUSTOMER_BRIDGE))
+        str = NM_LLDP_DEST_NEAREST_CUSTOMER_BRIDGE;
+    else
+        str = NULL;
+    if (str)
+        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_DESTINATION, str);
+
+    if (sd_lldp_neighbor_get_port_description(neigh->neighbor_sd, &str) == 0)
+        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_DESCRIPTION, str);
+
+    if (sd_lldp_neighbor_get_system_name(neigh->neighbor_sd, &str) == 0)
+        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_NAME, str);
+
+    if (sd_lldp_neighbor_get_system_description(neigh->neighbor_sd, &str) == 0)
+        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, str);
+
+    if (sd_lldp_neighbor_get_system_capabilities(neigh->neighbor_sd, &u16) == 0)
+        nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, u16);
+
+    r = sd_lldp_neighbor_tlv_rewind(neigh->neighbor_sd);
+    if (r < 0)
+        nm_assert_not_reached();
+    else {
+        gboolean        v_management_addresses_has = FALSE;
+        GVariantBuilder v_management_addresses;
+        GVariant *      v_ieee_802_1_pvid        = NULL;
+        GVariant *      v_ieee_802_1_ppvid       = NULL;
+        GVariant *      v_ieee_802_1_ppvid_flags = NULL;
+        GVariantBuilder v_ieee_802_1_ppvids;
+        GVariant *      v_ieee_802_1_vid       = NULL;
+        GVariant *      v_ieee_802_1_vlan_name = NULL;
+        GVariantBuilder v_ieee_802_1_vlans;
+        GVariant *      v_ieee_802_3_mac_phy_conf   = NULL;
+        GVariant *      v_ieee_802_3_power_via_mdi  = NULL;
+        GVariant *      v_ieee_802_3_max_frame_size = NULL;
+        GVariant *      v_mud_url                   = NULL;
+        GVariantBuilder tmp_builder;
+        GVariant *      tmp_variant;
+
+        do {
+            guint8 oui[3];
+            guint8 type;
+            guint8 subtype;
+
+            if (sd_lldp_neighbor_tlv_get_type(neigh->neighbor_sd, &type) < 0)
+                continue;
+
+            if (sd_lldp_neighbor_tlv_get_raw(neigh->neighbor_sd, (void *) &data8, &len) < 0)
+                continue;
+
+            switch (type) {
+            case SD_LLDP_TYPE_MGMT_ADDRESS:
+                tmp_variant = parse_management_address_tlv(data8, len);
+                if (tmp_variant) {
+                    if (!v_management_addresses_has) {
+                        v_management_addresses_has = TRUE;
+                        g_variant_builder_init(&v_management_addresses, G_VARIANT_TYPE("aa{sv}"));
+                    }
+                    g_variant_builder_add_value(&v_management_addresses, tmp_variant);
+                }
+                continue;
+            case SD_LLDP_TYPE_PRIVATE:
+                break;
+            default:
+                continue;
+            }
+
+            r = sd_lldp_neighbor_tlv_get_oui(neigh->neighbor_sd, oui, &subtype);
+            if (r < 0) {
+                if (r == -ENXIO)
+                    continue;
+
+                /* in other cases, something is seriously wrong. Abort, but
+                 * keep what we parsed so far. */
+                break;
+            }
+
+            if (len <= 6)
+                continue;
+
+                /* skip over leading TLV, OUI and subtype */
+#if NM_MORE_ASSERTS > 5
+            {
+                guint8 check_hdr[] = {0xfe | (((len - 2) >> 8) & 0x01),
+                                      ((len - 2) & 0xFF),
+                                      oui[0],
+                                      oui[1],
+                                      oui[2],
+                                      subtype};
+
+                nm_assert(len > 2 + 3 + 1);
+                nm_assert(memcmp(data8, check_hdr, sizeof check_hdr) == 0);
+            }
+#endif
+            data8 += 6;
+            len -= 6;
+
+            if (memcmp(oui, SD_LLDP_OUI_802_1, sizeof(oui)) == 0) {
+                switch (subtype) {
+                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID:
+                    if (len != 2)
+                        continue;
+                    if (!v_ieee_802_1_pvid)
+                        v_ieee_802_1_pvid = g_variant_new_uint32(unaligned_read_be16(data8));
+                    break;
+                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID:
+                    if (len != 3)
+                        continue;
+                    if (!v_ieee_802_1_ppvid) {
+                        v_ieee_802_1_ppvid_flags = g_variant_new_uint32(data8[0]);
+                        v_ieee_802_1_ppvid = g_variant_new_uint32(unaligned_read_be16(&data8[1]));
+                        g_variant_builder_init(&v_ieee_802_1_ppvids, G_VARIANT_TYPE("aa{sv}"));
+                    }
+                    g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
+                    nm_g_variant_builder_add_sv_uint32(&tmp_builder,
+                                                       "ppvid",
+                                                       unaligned_read_be16(&data8[1]));
+                    nm_g_variant_builder_add_sv_uint32(&tmp_builder, "flags", data8[0]);
+                    g_variant_builder_add_value(&v_ieee_802_1_ppvids,
+                                                g_variant_builder_end(&tmp_builder));
+                    break;
+                case SD_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME:
+                {
+                    gs_free char *name_to_free = NULL;
+                    const char *  name;
+                    guint32       vid;
+                    gsize         l;
+
+                    if (len <= 3)
+                        continue;
+
+                    l = data8[2];
+                    if (len != 3 + l)
+                        continue;
+                    if (l > 32)
+                        continue;
+
+                    name = format_string(&data8[3], l, TRUE, &name_to_free);
+                    if (!name)
+                        continue;
+
+                    vid = unaligned_read_be16(&data8[0]);
+                    if (!v_ieee_802_1_vid) {
+                        v_ieee_802_1_vid       = g_variant_new_uint32(vid);
+                        v_ieee_802_1_vlan_name = g_variant_new_string(name);
+                        g_variant_builder_init(&v_ieee_802_1_vlans, G_VARIANT_TYPE("aa{sv}"));
+                    }
+                    g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
+                    nm_g_variant_builder_add_sv_uint32(&tmp_builder, "vid", vid);
+                    nm_g_variant_builder_add_sv_str(&tmp_builder, "name", name);
+                    g_variant_builder_add_value(&v_ieee_802_1_vlans,
+                                                g_variant_builder_end(&tmp_builder));
+                    break;
+                }
+                default:
+                    continue;
+                }
+            } else if (memcmp(oui, SD_LLDP_OUI_802_3, sizeof(oui)) == 0) {
+                switch (subtype) {
+                case SD_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
+                    if (len != 5)
+                        continue;
+
+                    if (!v_ieee_802_3_mac_phy_conf) {
+                        g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder, "autoneg", data8[0]);
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
+                                                           "pmd-autoneg-cap",
+                                                           unaligned_read_be16(&data8[1]));
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
+                                                           "operational-mau-type",
+                                                           unaligned_read_be16(&data8[3]));
+                        v_ieee_802_3_mac_phy_conf = g_variant_builder_end(&tmp_builder);
+                    }
+                    break;
+                case SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
+                    if (len != 3)
+                        continue;
+
+                    if (!v_ieee_802_3_power_via_mdi) {
+                        g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
+                                                           "mdi-power-support",
+                                                           data8[0]);
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
+                                                           "pse-power-pair",
+                                                           data8[1]);
+                        nm_g_variant_builder_add_sv_uint32(&tmp_builder, "power-class", data8[2]);
+                        v_ieee_802_3_power_via_mdi = g_variant_builder_end(&tmp_builder);
+                    }
+                    break;
+                case SD_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
+                    if (len != 2)
+                        continue;
+                    if (!v_ieee_802_3_max_frame_size)
+                        v_ieee_802_3_max_frame_size =
+                            g_variant_new_uint32(unaligned_read_be16(data8));
+                    break;
+                }
+            } else if (memcmp(oui, SD_LLDP_OUI_MUD, sizeof(oui)) == 0) {
+                switch (subtype) {
+                case SD_LLDP_OUI_SUBTYPE_MUD_USAGE_DESCRIPTION:
+                    if (!v_mud_url) {
+                        gs_free char *s_free = NULL;
+                        const char *  s;
+
+                        s = format_string(data8, len, TRUE, &s_free);
+                        if (s)
+                            v_mud_url = g_variant_new_string(s);
+                    }
+                    break;
+                }
+            }
+        } while (sd_lldp_neighbor_tlv_next(neigh->neighbor_sd) > 0);
+
+        if (v_management_addresses_has)
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_MANAGEMENT_ADDRESSES,
+                                        g_variant_builder_end(&v_management_addresses));
+        if (v_ieee_802_1_pvid)
+            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_PVID, v_ieee_802_1_pvid);
+        if (v_ieee_802_1_ppvid) {
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_1_PPVID,
+                                        v_ieee_802_1_ppvid);
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS,
+                                        v_ieee_802_1_ppvid_flags);
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_1_PPVIDS,
+                                        g_variant_builder_end(&v_ieee_802_1_ppvids));
+        }
+        if (v_ieee_802_1_vid) {
+            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_VID, v_ieee_802_1_vid);
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME,
+                                        v_ieee_802_1_vlan_name);
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_1_VLANS,
+                                        g_variant_builder_end(&v_ieee_802_1_vlans));
+        }
+        if (v_ieee_802_3_mac_phy_conf)
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF,
+                                        v_ieee_802_3_mac_phy_conf);
+        if (v_ieee_802_3_power_via_mdi)
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI,
+                                        v_ieee_802_3_power_via_mdi);
+        if (v_ieee_802_3_max_frame_size)
+            nm_g_variant_builder_add_sv(&builder,
+                                        NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE,
+                                        v_ieee_802_3_max_frame_size);
+        if (v_mud_url)
+            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_MUD_URL, v_mud_url);
+    }
+
+    return (neigh->variant = g_variant_ref_sink(g_variant_builder_end(&builder)));
+}
+
+/*****************************************************************************/
+
+GVariant *
+nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len)
+{
+    nm_auto(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *neighbor_sd = NULL;
+    nm_auto(lldp_neighbor_freep) LldpNeighbor *        neigh       = NULL;
+    GVariant *                                         variant;
+    int                                                r;
+
+    g_assert(raw_data);
+    g_assert(raw_len > 0);
+
+    r = sd_lldp_neighbor_from_raw(&neighbor_sd, raw_data, raw_len);
+    g_assert(r >= 0);
+
+    neigh = lldp_neighbor_new(neighbor_sd);
+    g_assert(neigh);
+
+    variant = lldp_neighbor_to_variant(neigh);
+    g_assert(variant);
+
+    return g_variant_ref(variant);
+}
+
+/*****************************************************************************/
+
+static void
+data_changed_notify(NMLldpListener *self, NMLldpListenerPrivate *priv)
+{
+    nm_clear_g_variant(&priv->variant);
+    _notify(self, PROP_NEIGHBORS);
+}
+
+static gboolean
+data_changed_timeout(gpointer user_data)
+{
+    NMLldpListener *       self = user_data;
+    NMLldpListenerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), G_SOURCE_REMOVE);
+
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    priv->ratelimit_id        = 0;
+    priv->ratelimit_next_nsec = nm_utils_get_monotonic_timestamp_nsec() + MIN_UPDATE_INTERVAL_NSEC;
+    data_changed_notify(self, priv);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+data_changed_schedule(NMLldpListener *self)
+{
+    NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+    gint64                 now_nsec;
+
+    if (priv->ratelimit_id != 0)
+        return;
+
+    now_nsec = nm_utils_get_monotonic_timestamp_nsec();
+    if (now_nsec < priv->ratelimit_next_nsec) {
+        priv->ratelimit_id =
+            g_timeout_add_full(G_PRIORITY_LOW,
+                               NM_UTILS_NSEC_TO_MSEC_CEIL(priv->ratelimit_next_nsec - now_nsec),
+                               data_changed_timeout,
+                               self,
+                               NULL);
+        return;
+    }
+
+    priv->ratelimit_id = g_idle_add_full(G_PRIORITY_LOW, data_changed_timeout, self, NULL);
+}
+
+static void
+process_lldp_neighbor(NMLldpListener *self, sd_lldp_neighbor *neighbor_sd, gboolean remove)
+{
+    NMLldpListenerPrivate *                    priv;
+    nm_auto(lldp_neighbor_freep) LldpNeighbor *neigh = NULL;
+    LldpNeighbor *                             neigh_old;
+
+    g_return_if_fail(NM_IS_LLDP_LISTENER(self));
+
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->lldp_handle);
+    g_return_if_fail(neighbor_sd);
+
+    nm_assert(priv->lldp_neighbors);
+
+    neigh = lldp_neighbor_new(neighbor_sd);
+    if (!neigh) {
+        _LOGT("process: failed to parse neighbor");
+        return;
+    }
+
+    neigh_old = g_hash_table_lookup(priv->lldp_neighbors, neigh);
+
+    if (remove) {
+        if (neigh_old) {
+            _LOGT("process: %s neigh: " LOG_NEIGH_FMT, "remove", LOG_NEIGH_ARG(neigh));
+
+            g_hash_table_remove(priv->lldp_neighbors, neigh_old);
+            goto handle_changed;
+        }
+        return;
+    }
+
+    if (neigh_old && lldp_neighbor_equal(neigh_old, neigh))
+        return;
+
+    _LOGD("process: %s neigh: " LOG_NEIGH_FMT, neigh_old ? "update" : "new", LOG_NEIGH_ARG(neigh));
+
+    g_hash_table_add(priv->lldp_neighbors, g_steal_pointer(&neigh));
+
+handle_changed:
+    data_changed_schedule(self);
+}
+
+static void
+lldp_event_handler(sd_lldp *lldp, sd_lldp_event event, sd_lldp_neighbor *n, void *userdata)
+{
+    process_lldp_neighbor(
+        userdata,
+        n,
+        !NM_IN_SET(event, SD_LLDP_EVENT_ADDED, SD_LLDP_EVENT_UPDATED, SD_LLDP_EVENT_REFRESHED));
+}
+
+gboolean
+nm_lldp_listener_start(NMLldpListener *self, int ifindex, GError **error)
+{
+    NMLldpListenerPrivate *priv;
+    int                    ret;
+
+    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);
+    g_return_val_if_fail(ifindex > 0, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    if (priv->lldp_handle) {
+        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "already running");
+        return FALSE;
+    }
+
+    ret = sd_lldp_new(&priv->lldp_handle);
+    if (ret < 0) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_FAILED,
+                            "initialization failed");
+        return FALSE;
+    }
+
+    ret = sd_lldp_set_ifindex(priv->lldp_handle, ifindex);
+    if (ret < 0) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_FAILED,
+                            "failed setting ifindex");
+        goto err;
+    }
+
+    ret = sd_lldp_set_callback(priv->lldp_handle, lldp_event_handler, self);
+    if (ret < 0) {
+        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "set callback failed");
+        goto err;
+    }
+
+    ret = sd_lldp_set_neighbors_max(priv->lldp_handle, MAX_NEIGHBORS);
+    nm_assert(ret == 0);
+
+    priv->ifindex = ifindex;
+
+    ret = sd_lldp_attach_event(priv->lldp_handle, NULL, 0);
+    if (ret < 0) {
+        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "attach event failed");
+        goto err_free;
+    }
+
+    ret = sd_lldp_start(priv->lldp_handle);
+    if (ret < 0) {
+        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "start failed");
+        goto err;
+    }
+
+    priv->lldp_neighbors = g_hash_table_new_full(lldp_neighbor_id_hash,
+                                                 lldp_neighbor_id_equal,
+                                                 (GDestroyNotify) lldp_neighbor_free,
+                                                 NULL);
+
+    _LOGD("start");
+
+    return TRUE;
+
+err:
+    sd_lldp_detach_event(priv->lldp_handle);
+err_free:
+    sd_lldp_unref(priv->lldp_handle);
+    priv->lldp_handle = NULL;
+    priv->ifindex     = 0;
+    return FALSE;
+}
+
+void
+nm_lldp_listener_stop(NMLldpListener *self)
+{
+    NMLldpListenerPrivate *priv;
+    guint                  size;
+    gboolean               changed = FALSE;
+
+    g_return_if_fail(NM_IS_LLDP_LISTENER(self));
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    if (priv->lldp_handle) {
+        _LOGD("stop");
+        sd_lldp_stop(priv->lldp_handle);
+        sd_lldp_detach_event(priv->lldp_handle);
+        sd_lldp_unref(priv->lldp_handle);
+        priv->lldp_handle = NULL;
+
+        size = g_hash_table_size(priv->lldp_neighbors);
+        g_hash_table_remove_all(priv->lldp_neighbors);
+        nm_clear_pointer(&priv->lldp_neighbors, g_hash_table_unref);
+        if (size > 0 || priv->ratelimit_id != 0)
+            changed = TRUE;
+    }
+
+    nm_clear_g_source(&priv->ratelimit_id);
+    priv->ratelimit_next_nsec = 0;
+    priv->ifindex             = 0;
+
+    if (changed)
+        data_changed_notify(self, priv);
+}
+
+gboolean
+nm_lldp_listener_is_running(NMLldpListener *self)
+{
+    NMLldpListenerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);
+
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+    return !!priv->lldp_handle;
+}
+
+GVariant *
+nm_lldp_listener_get_neighbors(NMLldpListener *self)
+{
+    NMLldpListenerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);
+
+    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    if (G_UNLIKELY(!priv->variant)) {
+        gs_free LldpNeighbor **neighbors = NULL;
+        GVariantBuilder        array_builder;
+        guint                  i, n;
+
+        g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aa{sv}"));
+        neighbors = (LldpNeighbor **)
+            nm_utils_hash_keys_to_array(priv->lldp_neighbors, lldp_neighbor_id_cmp_p, NULL, &n);
+        for (i = 0; i < n; i++)
+            g_variant_builder_add_value(&array_builder, lldp_neighbor_to_variant(neighbors[i]));
+        priv->variant = g_variant_ref_sink(g_variant_builder_end(&array_builder));
+    }
+    return priv->variant;
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMLldpListener *self = NM_LLDP_LISTENER(object);
+
+    switch (prop_id) {
+    case PROP_NEIGHBORS:
+        g_value_set_variant(value, nm_lldp_listener_get_neighbors(self));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+nm_lldp_listener_init(NMLldpListener *self)
+{
+    _LOGT("lldp listener created");
+}
+
+NMLldpListener *
+nm_lldp_listener_new(void)
+{
+    return g_object_new(NM_TYPE_LLDP_LISTENER, NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    nm_lldp_listener_stop(NM_LLDP_LISTENER(object));
+
+    G_OBJECT_CLASS(nm_lldp_listener_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMLldpListener *       self = NM_LLDP_LISTENER(object);
+    NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
+
+    nm_lldp_listener_stop(self);
+
+    nm_clear_g_variant(&priv->variant);
+
+    _LOGT("lldp listener destroyed");
+
+    G_OBJECT_CLASS(nm_lldp_listener_parent_class)->finalize(object);
+}
+
+static void
+nm_lldp_listener_class_init(NMLldpListenerClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+    object_class->get_property = get_property;
+
+    obj_properties[PROP_NEIGHBORS] =
+        g_param_spec_variant(NM_LLDP_LISTENER_NEIGHBORS,
+                             "",
+                             "",
+                             G_VARIANT_TYPE("aa{sv}"),
+                             NULL,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/nm-lldp-listener.h b/src/core/devices/nm-lldp-listener.h
new file mode 100644
index 00000000..9d3e2436
--- /dev/null
+++ b/src/core/devices/nm-lldp-listener.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#ifndef __NM_LLDP_LISTENER__
+#define __NM_LLDP_LISTENER__
+
+#define NM_TYPE_LLDP_LISTENER (nm_lldp_listener_get_type())
+#define NM_LLDP_LISTENER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_LLDP_LISTENER, NMLldpListener))
+#define NM_LLDP_LISTENER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_LLDP_LISTENER, NMLldpListenerClass))
+#define NM_IS_LLDP_LISTENER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_LLDP_LISTENER))
+#define NM_IS_LLDP_LISTENER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_LLDP_LISTENER))
+#define NM_LLDP_LISTENER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_LLDP_LISTENER, NMLldpListenerClass))
+
+#define NM_LLDP_LISTENER_NEIGHBORS "neighbors"
+
+typedef struct _NMLldpListenerClass NMLldpListenerClass;
+
+GType           nm_lldp_listener_get_type(void);
+NMLldpListener *nm_lldp_listener_new(void);
+gboolean        nm_lldp_listener_start(NMLldpListener *self, int ifindex, GError **error);
+void            nm_lldp_listener_stop(NMLldpListener *self);
+gboolean        nm_lldp_listener_is_running(NMLldpListener *self);
+
+GVariant *nm_lldp_listener_get_neighbors(NMLldpListener *self);
+
+GVariant *nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len);
+
+#endif /* __NM_LLDP_LISTENER__ */
diff --git a/src/core/devices/ovs/meson.build b/src/core/devices/ovs/meson.build
new file mode 100644
index 00000000..81c29bd6
--- /dev/null
+++ b/src/core/devices/ovs/meson.build
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_device_plugin_ovs = shared_module(
+  'nm-device-plugin-ovs',
+  sources: files(
+    'nm-device-ovs-bridge.c',
+    'nm-device-ovs-interface.c',
+    'nm-device-ovs-port.c',
+    'nm-ovsdb.c',
+    'nm-ovs-factory.c',
+  ),
+  dependencies: [
+    core_plugin_dep,
+    jansson_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_ovs
+
+test(
+  'check-local-devices-ovs',
+  check_exports,
+  args: [
+    libnm_device_plugin_ovs.full_path(),
+    linker_script_devices,
+  ],
+)
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
new file mode 100644
index 00000000..3ae8a481
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ovs-bridge.h"
+
+#include "nm-device-ovs-interface.h"
+#include "nm-device-ovs-port.h"
+#include "nm-ovsdb.h"
+
+#include "devices/nm-device-private.h"
+#include "nm-active-connection.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-ovs-bridge.h"
+#include "nm-setting-ovs-external-ids.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceOvsBridge
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceOvsBridge {
+    NMDevice parent;
+};
+
+struct _NMDeviceOvsBridgeClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceOvsBridge, nm_device_ovs_bridge, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    return "ovs-bridge";
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    /* The actual backing resources will be created on enslavement by the port
+     * when it can identify the port and the bridge. */
+
+    return TRUE;
+}
+
+static gboolean
+unrealize(NMDevice *device, GError **error)
+{
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    return NM_ACT_STAGE_RETURN_IP_FAIL;
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    if (!configure)
+        return TRUE;
+
+    if (!NM_IS_DEVICE_OVS_PORT(slave))
+        return FALSE;
+
+    return TRUE;
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{}
+
+void
+nm_device_ovs_reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceType device_type;
+    GType        type;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(g_type_parent(G_TYPE_FROM_INSTANCE(self)) == NM_TYPE_DEVICE);
+
+    /* NMDevice's reapply_connection() doesn't do anything. No need to call the parent
+     * implementation. */
+
+    _LOGD(LOGD_DEVICE, "reapplying settings for OVS device");
+
+    type = G_OBJECT_TYPE(self);
+    if (type == NM_TYPE_DEVICE_OVS_INTERFACE)
+        device_type = NM_DEVICE_TYPE_OVS_INTERFACE;
+    else if (type == NM_TYPE_DEVICE_OVS_PORT)
+        device_type = NM_DEVICE_TYPE_OVS_PORT;
+    else {
+        nm_assert(type == NM_TYPE_DEVICE_OVS_BRIDGE);
+        device_type = NM_DEVICE_TYPE_OVS_BRIDGE;
+    }
+
+    nm_ovsdb_set_external_ids(
+        nm_ovsdb_get(),
+        device_type,
+        nm_device_get_ip_iface(self),
+        nm_connection_get_uuid(con_new),
+        _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_EXTERNAL_IDS),
+        _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_EXTERNAL_IDS));
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_ovs_bridge_init(NMDeviceOvsBridge *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_ovs_bridge = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_OVS_BRIDGE,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ),
+        .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ovs_bridge_class_init(NMDeviceOvsBridgeClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_bridge);
+
+    device_class->connection_type_supported        = NM_SETTING_OVS_BRIDGE_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_OVS_BRIDGE_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES();
+
+    device_class->is_master                           = TRUE;
+    device_class->get_type_description                = get_type_description;
+    device_class->create_and_realize                  = create_and_realize;
+    device_class->unrealize                           = unrealize;
+    device_class->get_generic_capabilities            = get_generic_capabilities;
+    device_class->act_stage3_ip_config_start          = act_stage3_ip_config_start;
+    device_class->enslave_slave                       = enslave_slave;
+    device_class->release_slave                       = release_slave;
+    device_class->can_reapply_change_ovs_external_ids = TRUE;
+    device_class->reapply_connection                  = nm_device_ovs_reapply_connection;
+}
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.h b/src/core/devices/ovs/nm-device-ovs-bridge.h
new file mode 100644
index 00000000..2b893343
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__
+#define __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__
+
+#define NM_TYPE_DEVICE_OVS_BRIDGE (nm_device_ovs_bridge_get_type())
+#define NM_DEVICE_OVS_BRIDGE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridge))
+#define NM_DEVICE_OVS_BRIDGE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridgeClass))
+#define NM_IS_DEVICE_OVS_BRIDGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_BRIDGE))
+#define NM_IS_DEVICE_OVS_BRIDGE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_BRIDGE))
+#define NM_DEVICE_OVS_BRIDGE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_BRIDGE, NMDeviceOvsBridgeClass))
+
+typedef struct _NMDeviceOvsBridge      NMDeviceOvsBridge;
+typedef struct _NMDeviceOvsBridgeClass NMDeviceOvsBridgeClass;
+
+GType nm_device_ovs_bridge_get_type(void);
+
+/*****************************************************************************/
+
+void
+nm_device_ovs_reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new);
+
+#endif /* __NETWORKMANAGER_DEVICE_OVS_BRIDGE_H__ */
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c
new file mode 100644
index 00000000..5d07c211
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-interface.c
@@ -0,0 +1,445 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ovs-interface.h"
+
+#include "nm-device-ovs-bridge.h"
+#include "nm-ovsdb.h"
+
+#include "devices/nm-device-private.h"
+#include "nm-active-connection.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-ovs-interface.h"
+#include "nm-setting-ovs-port.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceOvsInterface
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    NMOvsdb *ovsdb;
+    bool     waiting_for_interface : 1;
+} NMDeviceOvsInterfacePrivate;
+
+struct _NMDeviceOvsInterface {
+    NMDevice                    parent;
+    NMDeviceOvsInterfacePrivate _priv;
+};
+
+struct _NMDeviceOvsInterfaceClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceOvsInterface, nm_device_ovs_interface, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceOvsInterface, NM_IS_DEVICE_OVS_INTERFACE, NMDevice)
+
+/*****************************************************************************/
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    return "ovs-interface";
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    /* The actual backing resources will be created once an interface is
+     * added to a port of ours, since there can be neither an empty port nor
+     * an empty bridge. */
+
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    return nm_ovsdb_is_ready(priv->ovsdb);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMSettingOvsInterface *s_ovs_iface;
+
+    if (!NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_ovs_iface = nm_connection_get_setting_ovs_interface(connection);
+
+    if (!NM_IN_STRSET(nm_setting_ovs_interface_get_interface_type(s_ovs_iface),
+                      "dpdk",
+                      "internal",
+                      "patch")) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "unsupported OVS interface type in profile");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device);
+
+    if (!pllink || !priv->waiting_for_interface)
+        return;
+
+    priv->waiting_for_interface = FALSE;
+
+    if (nm_device_get_state(device) == NM_DEVICE_STATE_IP_CONFIG) {
+        if (!nm_device_hw_addr_set_cloned(device,
+                                          nm_device_get_applied_connection(device),
+                                          FALSE)) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            return;
+        }
+        nm_device_bring_up(device, TRUE, NULL);
+        nm_device_activate_schedule_stage3_ip_config_start(device);
+    }
+}
+
+static gboolean
+_is_internal_interface(NMDevice *device)
+{
+    NMSettingOvsInterface *s_ovs_iface;
+
+    s_ovs_iface = nm_device_get_applied_setting(device, NM_TYPE_SETTING_OVS_INTERFACE);
+
+    g_return_val_if_fail(s_ovs_iface, FALSE);
+
+    return nm_streq(nm_setting_ovs_interface_get_interface_type(s_ovs_iface), "internal");
+}
+
+static void
+set_platform_mtu_cb(GError *error, gpointer user_data)
+{
+    NMDevice *            device = user_data;
+    NMDeviceOvsInterface *self   = NM_DEVICE_OVS_INTERFACE(device);
+
+    if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) {
+        _LOGW(LOGD_DEVICE,
+              "could not change mtu of '%s': %s",
+              nm_device_get_iface(device),
+              error->message);
+    }
+
+    g_object_unref(device);
+}
+
+static gboolean
+set_platform_mtu(NMDevice *device, guint32 mtu)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    /*
+     * If the MTU is not set in ovsdb, Open vSwitch will change
+     * the MTU of an internal interface to match the minimum of
+     * the other interfaces in the bridge.
+     */
+    /* FIXME(shutdown): the function should become cancellable so
+     * that it doesn't need to hold a reference to the device, and
+     * it can be stopped during shutdown.
+     */
+    if (_is_internal_interface(device)) {
+        nm_ovsdb_set_interface_mtu(priv->ovsdb,
+                                   nm_device_get_ip_iface(device),
+                                   mtu,
+                                   set_platform_mtu_cb,
+                                   g_object_ref(device));
+    }
+
+    return NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class)->set_platform_mtu(device, mtu);
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device);
+
+    if (!_is_internal_interface(device))
+        return NM_ACT_STAGE_RETURN_IP_FAIL;
+
+    if (nm_device_get_ip_ifindex(device) <= 0) {
+        _LOGT(LOGD_DEVICE, "waiting for link to appear");
+        priv->waiting_for_interface = TRUE;
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) {
+        *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static gboolean
+can_unmanaged_external_down(NMDevice *self)
+{
+    return FALSE;
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    priv->waiting_for_interface = FALSE;
+}
+
+typedef struct {
+    NMDeviceOvsInterface *     self;
+    GCancellable *             cancellable;
+    NMDeviceDeactivateCallback callback;
+    gpointer                   callback_user_data;
+    gulong                     link_changed_id;
+    gulong                     cancelled_id;
+    guint                      link_timeout_id;
+} DeactivateData;
+
+static void
+deactivate_invoke_cb(DeactivateData *data, GError *error)
+{
+    NMDeviceOvsInterface *self = data->self;
+
+    _LOGT(LOGD_CORE, "deactivate: async callback (%s)", error ? error->message : "success");
+    data->callback(NM_DEVICE(data->self), error, data->callback_user_data);
+
+    nm_clear_g_signal_handler(nm_device_get_platform(NM_DEVICE(data->self)),
+                              &data->link_changed_id);
+    nm_clear_g_signal_handler(data->cancellable, &data->cancelled_id);
+    nm_clear_g_source(&data->link_timeout_id);
+    g_object_unref(data->self);
+    g_object_unref(data->cancellable);
+    nm_g_slice_free(data);
+}
+
+static void
+deactivate_link_changed_cb(NMPlatform *    platform,
+                           int             obj_type_i,
+                           int             ifindex,
+                           NMPlatformLink *info,
+                           int             change_type_i,
+                           DeactivateData *data)
+{
+    NMDeviceOvsInterface *           self        = data->self;
+    const NMPlatformSignalChangeType change_type = change_type_i;
+
+    if (change_type == NM_PLATFORM_SIGNAL_REMOVED
+        && nm_streq0(info->name, nm_device_get_iface(NM_DEVICE(self)))) {
+        _LOGT(LOGD_DEVICE, "deactivate: link removed, proceeding");
+        nm_device_update_from_platform_link(NM_DEVICE(self), NULL);
+        deactivate_invoke_cb(data, NULL);
+        return;
+    }
+}
+
+static gboolean
+deactivate_link_timeout(gpointer user_data)
+{
+    DeactivateData *      data = user_data;
+    NMDeviceOvsInterface *self = data->self;
+
+    _LOGT(LOGD_DEVICE, "deactivate: timeout waiting link removal");
+    deactivate_invoke_cb(data, NULL);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+deactivate_cancelled_cb(GCancellable *cancellable, gpointer user_data)
+{
+    gs_free_error GError *error = NULL;
+
+    nm_utils_error_set_cancelled(&error, FALSE, NULL);
+    deactivate_invoke_cb((DeactivateData *) user_data, error);
+}
+
+static void
+deactivate_cb_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    DeactivateData *data                  = user_data;
+    gs_free_error GError *cancelled_error = NULL;
+
+    g_cancellable_set_error_if_cancelled(data->cancellable, &cancelled_error);
+    deactivate_invoke_cb(data, cancelled_error);
+}
+
+static void
+deactivate_async(NMDevice *                 device,
+                 GCancellable *             cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   callback_user_data)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+    DeactivateData *             data;
+
+    _LOGT(LOGD_CORE, "deactivate: start async");
+
+    /* We want to ensure that the kernel link for this device is
+     * removed upon disconnection so that it will not interfere with
+     * later activations of the same device. Unfortunately there is
+     * no synchronization mechanism with vswitchd, we only update
+     * ovsdb and wait that changes are picked up.
+     */
+
+    data  = g_slice_new(DeactivateData);
+    *data = (DeactivateData){
+        .self               = g_object_ref(self),
+        .cancellable        = g_object_ref(cancellable),
+        .callback           = callback,
+        .callback_user_data = callback_user_data,
+    };
+
+    if (!priv->waiting_for_interface
+        && !nm_platform_link_get_by_ifname(nm_device_get_platform(device),
+                                           nm_device_get_iface(device))) {
+        _LOGT(LOGD_CORE, "deactivate: link not present, proceeding");
+        nm_device_update_from_platform_link(NM_DEVICE(self), NULL);
+        nm_utils_invoke_on_idle(cancellable, deactivate_cb_on_idle, data);
+        return;
+    }
+
+    if (priv->waiting_for_interface) {
+        /* At this point we have issued an INSERT and a DELETE
+         * command for the interface to ovsdb. We don't know if
+         * vswitchd will see the two updates or only one. We
+         * must add a timeout to avoid waiting forever in case
+         * the link doesn't appear.
+         */
+        data->link_timeout_id = g_timeout_add(6000, deactivate_link_timeout, data);
+        _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear in 6 seconds");
+    } else
+        _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear");
+
+    data->cancelled_id =
+        g_cancellable_connect(cancellable, G_CALLBACK(deactivate_cancelled_cb), data, NULL);
+    data->link_changed_id = g_signal_connect(nm_device_get_platform(device),
+                                             NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                             G_CALLBACK(deactivate_link_changed_cb),
+                                             data);
+}
+
+static gboolean
+can_update_from_platform_link(NMDevice *device, const NMPlatformLink *plink)
+{
+    /* If the device is deactivating, we already sent the
+     * deletion command to ovsdb and we don't want to deal
+     * with any new link appearing from the previous
+     * activation.
+     */
+    return !plink || nm_device_get_state(device) != NM_DEVICE_STATE_DEACTIVATING;
+}
+
+/*****************************************************************************/
+
+static void
+ovsdb_ready(NMOvsdb *ovsdb, NMDeviceOvsInterface *self)
+{
+    NMDevice *device = NM_DEVICE(self);
+
+    nm_device_queue_recheck_available(device,
+                                      NM_DEVICE_STATE_REASON_NONE,
+                                      NM_DEVICE_STATE_REASON_NONE);
+    nm_device_recheck_available_connections(device);
+    nm_device_emit_recheck_auto_activate(device);
+}
+
+static void
+nm_device_ovs_interface_init(NMDeviceOvsInterface *self)
+{
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    priv->ovsdb = g_object_ref(nm_ovsdb_get());
+
+    if (!nm_ovsdb_is_ready(priv->ovsdb))
+        g_signal_connect(priv->ovsdb, NM_OVSDB_READY, G_CALLBACK(ovsdb_ready), self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceOvsInterface *       self = NM_DEVICE_OVS_INTERFACE(object);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    if (priv->ovsdb) {
+        g_signal_handlers_disconnect_by_func(priv->ovsdb, G_CALLBACK(ovsdb_ready), self);
+        g_clear_object(&priv->ovsdb);
+    }
+
+    G_OBJECT_CLASS(nm_device_ovs_interface_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_ovs_interface = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_OVS_INTERFACE,
+        .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ovs_interface_class_init(NMDeviceOvsInterfaceClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    dbus_object_class->interface_infos =
+        NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_interface);
+
+    device_class->connection_type_supported        = NM_SETTING_OVS_INTERFACE_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_OVS_INTERFACE_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_OPENVSWITCH);
+
+    device_class->can_update_from_platform_link       = can_update_from_platform_link;
+    device_class->deactivate                          = deactivate;
+    device_class->deactivate_async                    = deactivate_async;
+    device_class->get_type_description                = get_type_description;
+    device_class->create_and_realize                  = create_and_realize;
+    device_class->get_generic_capabilities            = get_generic_capabilities;
+    device_class->is_available                        = is_available;
+    device_class->check_connection_compatible         = check_connection_compatible;
+    device_class->link_changed                        = link_changed;
+    device_class->act_stage3_ip_config_start          = act_stage3_ip_config_start;
+    device_class->can_unmanaged_external_down         = can_unmanaged_external_down;
+    device_class->set_platform_mtu                    = set_platform_mtu;
+    device_class->get_configured_mtu                  = nm_device_get_configured_mtu_for_wired;
+    device_class->can_reapply_change_ovs_external_ids = TRUE;
+    device_class->reapply_connection                  = nm_device_ovs_reapply_connection;
+}
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.h b/src/core/devices/ovs/nm-device-ovs-interface.h
new file mode 100644
index 00000000..03b0e9a4
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-interface.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__
+#define __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__
+
+#define NM_TYPE_DEVICE_OVS_INTERFACE (nm_device_ovs_interface_get_type())
+#define NM_DEVICE_OVS_INTERFACE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterface))
+#define NM_DEVICE_OVS_INTERFACE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterfaceClass))
+#define NM_IS_DEVICE_OVS_INTERFACE(obj) \
+    (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_INTERFACE))
+#define NM_IS_DEVICE_OVS_INTERFACE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_INTERFACE))
+#define NM_DEVICE_OVS_INTERFACE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_INTERFACE, NMDeviceOvsInterfaceClass))
+
+typedef struct _NMDeviceOvsInterface      NMDeviceOvsInterface;
+typedef struct _NMDeviceOvsInterfaceClass NMDeviceOvsInterfaceClass;
+
+GType nm_device_ovs_interface_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_OVS_INTERFACE_H__ */
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
new file mode 100644
index 00000000..2ecb95e8
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -0,0 +1,196 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-ovs-port.h"
+
+#include "nm-device-ovs-interface.h"
+#include "nm-device-ovs-bridge.h"
+#include "nm-ovsdb.h"
+
+#include "devices/nm-device-private.h"
+#include "nm-active-connection.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-ovs-port.h"
+#include "nm-setting-ovs-port.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceOvsPort
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceOvsPort {
+    NMDevice parent;
+};
+
+struct _NMDeviceOvsPortClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceOvsPort, nm_device_ovs_port, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    return "ovs-port";
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    /* The port will be added to ovsdb when an interface is enslaved,
+     * because there's no such thing like an empty port. */
+
+    return TRUE;
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    return NM_ACT_STAGE_RETURN_IP_FAIL;
+}
+
+static void
+add_iface_cb(GError *error, gpointer user_data)
+{
+    NMDevice *slave = user_data;
+
+    if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) {
+        nm_log_warn(LOGD_DEVICE,
+                    "device %s could not be added to a ovs port: %s",
+                    nm_device_get_iface(slave),
+                    error->message);
+        nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+    }
+
+    g_object_unref(slave);
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    NMDeviceOvsPort *   self      = NM_DEVICE_OVS_PORT(device);
+    NMActiveConnection *ac_port   = NULL;
+    NMActiveConnection *ac_bridge = NULL;
+    NMDevice *          bridge_device;
+
+    if (!configure)
+        return TRUE;
+
+    ac_port   = NM_ACTIVE_CONNECTION(nm_device_get_act_request(device));
+    ac_bridge = nm_active_connection_get_master(ac_port);
+    if (!ac_bridge) {
+        _LOGW(LOGD_DEVICE,
+              "can't enslave %s: bridge active-connection not found",
+              nm_device_get_iface(slave));
+        return FALSE;
+    }
+
+    bridge_device = nm_active_connection_get_device(ac_bridge);
+    if (!bridge_device) {
+        _LOGW(LOGD_DEVICE, "can't enslave %s: bridge device not found", nm_device_get_iface(slave));
+        return FALSE;
+    }
+
+    nm_ovsdb_add_interface(nm_ovsdb_get(),
+                           nm_active_connection_get_applied_connection(ac_bridge),
+                           nm_device_get_applied_connection(device),
+                           nm_device_get_applied_connection(slave),
+                           bridge_device,
+                           slave,
+                           add_iface_cb,
+                           g_object_ref(slave));
+
+    return TRUE;
+}
+
+static void
+del_iface_cb(GError *error, gpointer user_data)
+{
+    NMDevice *slave = user_data;
+
+    if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) {
+        nm_log_warn(LOGD_DEVICE,
+                    "device %s could not be removed from a ovs port: %s",
+                    nm_device_get_iface(slave),
+                    error->message);
+        nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+    }
+
+    g_object_unref(slave);
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{
+    NMDeviceOvsPort *self = NM_DEVICE_OVS_PORT(device);
+
+    if (configure) {
+        _LOGI(LOGD_DEVICE, "releasing ovs interface %s", nm_device_get_ip_iface(slave));
+        nm_ovsdb_del_interface(nm_ovsdb_get(),
+                               nm_device_get_iface(slave),
+                               del_iface_cb,
+                               g_object_ref(slave));
+        /* Open VSwitch is going to delete this one. We must ignore what happens
+         * next with the interface. */
+        if (NM_IS_DEVICE_OVS_INTERFACE(slave))
+            nm_device_update_from_platform_link(slave, NULL);
+    } else
+        _LOGI(LOGD_DEVICE, "ovs interface %s was released", nm_device_get_ip_iface(slave));
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_ovs_port_init(NMDeviceOvsPort *self)
+{}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_ovs_port = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_OVS_PORT,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ),
+        .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_ovs_port_class_init(NMDeviceOvsPortClass *klass)
+{
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_ovs_port);
+
+    device_class->connection_type_supported        = NM_SETTING_OVS_PORT_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_OVS_PORT_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES();
+
+    device_class->is_master                           = TRUE;
+    device_class->get_type_description                = get_type_description;
+    device_class->create_and_realize                  = create_and_realize;
+    device_class->get_generic_capabilities            = get_generic_capabilities;
+    device_class->act_stage3_ip_config_start          = act_stage3_ip_config_start;
+    device_class->enslave_slave                       = enslave_slave;
+    device_class->release_slave                       = release_slave;
+    device_class->can_reapply_change_ovs_external_ids = TRUE;
+    device_class->reapply_connection                  = nm_device_ovs_reapply_connection;
+}
diff --git a/src/core/devices/ovs/nm-device-ovs-port.h b/src/core/devices/ovs/nm-device-ovs-port.h
new file mode 100644
index 00000000..f4516c01
--- /dev/null
+++ b/src/core/devices/ovs/nm-device-ovs-port.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_OVS_PORT_H__
+#define __NETWORKMANAGER_DEVICE_OVS_PORT_H__
+
+#define NM_TYPE_DEVICE_OVS_PORT (nm_device_ovs_port_get_type())
+#define NM_DEVICE_OVS_PORT(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPort))
+#define NM_DEVICE_OVS_PORT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPortClass))
+#define NM_IS_DEVICE_OVS_PORT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OVS_PORT))
+#define NM_IS_DEVICE_OVS_PORT_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OVS_PORT))
+#define NM_DEVICE_OVS_PORT_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OVS_PORT, NMDeviceOvsPortClass))
+
+typedef struct _NMDeviceOvsPort      NMDeviceOvsPort;
+typedef struct _NMDeviceOvsPortClass NMDeviceOvsPortClass;
+
+GType nm_device_ovs_port_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_OVS_PORT_H__ */
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
new file mode 100644
index 00000000..e7af38d8
--- /dev/null
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -0,0 +1,324 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-manager.h"
+#include "nm-ovsdb.h"
+#include "nm-device-ovs-interface.h"
+#include "nm-device-ovs-port.h"
+#include "nm-device-ovs-bridge.h"
+#include "platform/nm-platform.h"
+#include "nm-core-internal.h"
+#include "settings/nm-settings.h"
+#include "devices/nm-device-factory.h"
+#include "devices/nm-device-private.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    NMDeviceFactory parent;
+} NMOvsFactory;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMOvsFactoryClass;
+
+#define NM_TYPE_OVS_FACTORY (nm_ovs_factory_get_type())
+#define NM_OVS_FACTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVS_FACTORY, NMOvsFactory))
+#define NM_OVS_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVS_FACTORY, NMOvsFactoryClass))
+#define NM_IS_OVS_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVS_FACTORY))
+#define NM_IS_OVS_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_OVS_FACTORY))
+#define NM_OVS_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_OVS_FACTORY, NMOvsFactoryClass))
+
+static GType nm_ovs_factory_get_type(void);
+G_DEFINE_TYPE(NMOvsFactory, nm_ovs_factory, NM_TYPE_DEVICE_FACTORY)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN LOGD_DEVICE
+#define _NMLOG(level, ifname, con_uuid, ...)                                                  \
+    G_STMT_START                                                                              \
+    {                                                                                         \
+        nm_log((level),                                                                       \
+               _NMLOG_DOMAIN,                                                                 \
+               (ifname),                                                                      \
+               (con_uuid),                                                                    \
+               "ovs: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                                                         \
+    G_STMT_END
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_OPENVSWITCH)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_OVS_BRIDGE_SETTING_NAME,
+                                                NM_SETTING_OVS_INTERFACE_SETTING_NAME,
+                                                NM_SETTING_OVS_PORT_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    nm_manager_set_capability(NM_MANAGER_GET, NM_CAPABILITY_OVS);
+    return g_object_new(NM_TYPE_OVS_FACTORY, NULL);
+}
+
+static NMDevice *
+new_device_from_type(const char *name, NMDeviceType device_type)
+{
+    GType       type;
+    const char *type_desc;
+    NMLinkType  link_type = NM_LINK_TYPE_NONE;
+
+    if (nm_manager_get_device(NM_MANAGER_GET, name, device_type))
+        return NULL;
+
+    if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE) {
+        type      = NM_TYPE_DEVICE_OVS_INTERFACE;
+        type_desc = "Open vSwitch Interface";
+        link_type = NM_LINK_TYPE_OPENVSWITCH;
+    } else if (device_type == NM_DEVICE_TYPE_OVS_PORT) {
+        type      = NM_TYPE_DEVICE_OVS_PORT;
+        type_desc = "Open vSwitch Port";
+    } else if (device_type == NM_DEVICE_TYPE_OVS_BRIDGE) {
+        type      = NM_TYPE_DEVICE_OVS_BRIDGE;
+        type_desc = "Open vSwitch Bridge";
+    } else {
+        return NULL;
+    }
+
+    return g_object_new(type,
+                        NM_DEVICE_IFACE,
+                        name,
+                        NM_DEVICE_DRIVER,
+                        "openvswitch",
+                        NM_DEVICE_DEVICE_TYPE,
+                        device_type,
+                        NM_DEVICE_TYPE_DESC,
+                        type_desc,
+                        NM_DEVICE_LINK_TYPE,
+                        link_type,
+                        NULL);
+}
+
+static void
+ovsdb_device_added(NMOvsdb *        ovsdb,
+                   const char *     name,
+                   guint            device_type_i,
+                   const char *     subtype,
+                   NMDeviceFactory *self)
+{
+    const NMDeviceType device_type = device_type_i;
+    NMDevice *         device;
+
+    if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE
+        && !NM_IN_STRSET(subtype, "internal", "patch")) {
+        /* system interfaces refer to kernel devices and
+         * don't need to be created by this factory. Ignore
+         * anything that is not an internal or patch
+         * interface. */
+        return;
+    }
+
+    device = new_device_from_type(name, device_type);
+    if (!device)
+        return;
+
+    g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
+    g_object_unref(device);
+}
+
+static void
+ovsdb_device_removed(NMOvsdb *        ovsdb,
+                     const char *     name,
+                     guint            device_type_i,
+                     const char *     subtype,
+                     NMDeviceFactory *self)
+{
+    const NMDeviceType device_type = device_type_i;
+    NMDevice *         device      = NULL;
+    NMDeviceState      device_state;
+    gboolean           is_system_interface = FALSE;
+
+    if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE
+        && !NM_IN_STRSET(subtype, "internal", "patch", "system"))
+        return;
+
+    if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE && nm_streq0(subtype, "system")) {
+        NMDevice *             d;
+        const CList *          list;
+        NMSettingOvsInterface *s_ovs_int;
+
+        /* The device associated to an OVS system interface can be of
+         * any kind. Find an interface with the same name and which has
+         * the OVS-interface setting. */
+        is_system_interface = TRUE;
+        nm_manager_for_each_device (NM_MANAGER_GET, d, list) {
+            if (!nm_streq0(nm_device_get_iface(d), name))
+                continue;
+            s_ovs_int = nm_device_get_applied_setting(d, NM_TYPE_SETTING_OVS_INTERFACE);
+            if (!s_ovs_int)
+                continue;
+            if (!nm_streq0(nm_setting_ovs_interface_get_interface_type(s_ovs_int), "system"))
+                continue;
+            /* Failing the system interface device is almost always the right
+             * thing to do when the ovsdb entry is removed. However, to avoid
+             * that a late device-removed signal tears down a different,
+             * newly-activated connection, let's also check that we have a master.
+             * Or in alternative, that the device is assumed/external: in such
+             * case it's always fine to fail the device.
+             */
+            if (!nm_device_get_master(d) && !nm_device_sys_iface_state_is_external_or_assume(d))
+                continue;
+
+            device = d;
+        }
+    } else {
+        device = nm_manager_get_device(NM_MANAGER_GET, name, device_type);
+    }
+
+    if (!device)
+        return;
+
+    device_state = nm_device_get_state(device);
+
+    if (device_type == NM_DEVICE_TYPE_OVS_INTERFACE && nm_device_get_act_request(device)
+        && device_state < NM_DEVICE_STATE_DEACTIVATING) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_DEACTIVATING,
+                                NM_DEVICE_STATE_REASON_REMOVED);
+        return;
+    }
+
+    /* OVS system interfaces still exist even without the ovsdb entry */
+    if (!is_system_interface && device_state == NM_DEVICE_STATE_UNMANAGED) {
+        nm_device_unrealize(device, TRUE, NULL);
+    }
+}
+
+static void
+ovsdb_interface_failed(NMOvsdb *        ovsdb,
+                       const char *     name,
+                       const char *     connection_uuid,
+                       const char *     error,
+                       NMDeviceFactory *self)
+{
+    NMDevice *             device     = NULL;
+    NMSettingsConnection * connection = NULL;
+    NMConnection *         c;
+    const char *           type;
+    NMSettingOvsInterface *s_ovs_int;
+    gboolean               is_patch = FALSE;
+    gboolean               ignore;
+
+    device = nm_manager_get_device(NM_MANAGER_GET, name, NM_DEVICE_TYPE_OVS_INTERFACE);
+    if (device && connection_uuid) {
+        connection =
+            nm_settings_get_connection_by_uuid(nm_device_get_settings(device), connection_uuid);
+    }
+
+    /* The patch interface which gets created first is expected to
+     * fail because the second patch doesn't exist yet. Ignore all
+     * failures of patch interfaces. */
+    if (connection && (c = nm_settings_connection_get_connection(connection))
+        && (type = nm_connection_get_connection_type(c))
+        && nm_streq0(type, NM_SETTING_OVS_INTERFACE_SETTING_NAME)
+        && (s_ovs_int = nm_connection_get_setting_ovs_interface(c))
+        && nm_streq0(nm_setting_ovs_interface_get_interface_type(s_ovs_int), "patch"))
+        is_patch = TRUE;
+
+    ignore = !device || is_patch;
+
+    _NMLOG(ignore ? LOGL_DEBUG : LOGL_INFO,
+           name,
+           connection_uuid,
+           "ovs interface \"%s\" (%s) failed%s: %s",
+           name,
+           connection_uuid,
+           ignore ? " (ignored)" : "",
+           error);
+
+    if (ignore)
+        return;
+
+    if (connection) {
+        nm_settings_connection_autoconnect_blocked_reason_set(
+            connection,
+            NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+            TRUE);
+    }
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+}
+
+static void
+start(NMDeviceFactory *self)
+{
+    NMOvsdb *ovsdb;
+
+    ovsdb = nm_ovsdb_get();
+
+    g_signal_connect_object(ovsdb,
+                            NM_OVSDB_DEVICE_ADDED,
+                            G_CALLBACK(ovsdb_device_added),
+                            self,
+                            (GConnectFlags) 0);
+    g_signal_connect_object(ovsdb,
+                            NM_OVSDB_DEVICE_REMOVED,
+                            G_CALLBACK(ovsdb_device_removed),
+                            self,
+                            (GConnectFlags) 0);
+    g_signal_connect_object(ovsdb,
+                            NM_OVSDB_INTERFACE_FAILED,
+                            G_CALLBACK(ovsdb_interface_failed),
+                            self,
+                            (GConnectFlags) 0);
+}
+
+static NMDevice *
+create_device(NMDeviceFactory *     self,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    NMDeviceType device_type     = NM_DEVICE_TYPE_UNKNOWN;
+    const char * connection_type = NULL;
+
+    if (g_strcmp0(iface, "ovs-system") == 0) {
+        *out_ignore = TRUE;
+        return NULL;
+    }
+
+    if (connection)
+        connection_type = nm_connection_get_connection_type(connection);
+
+    if (plink)
+        device_type = NM_DEVICE_TYPE_OVS_INTERFACE;
+    else if (g_strcmp0(connection_type, NM_SETTING_OVS_INTERFACE_SETTING_NAME) == 0)
+        device_type = NM_DEVICE_TYPE_OVS_INTERFACE;
+    else if (g_strcmp0(connection_type, NM_SETTING_OVS_PORT_SETTING_NAME) == 0)
+        device_type = NM_DEVICE_TYPE_OVS_PORT;
+    else if (g_strcmp0(connection_type, NM_SETTING_OVS_BRIDGE_SETTING_NAME) == 0)
+        device_type = NM_DEVICE_TYPE_OVS_BRIDGE;
+
+    return new_device_from_type(iface, device_type);
+}
+
+static void
+nm_ovs_factory_init(NMOvsFactory *self)
+{}
+
+static void
+nm_ovs_factory_class_init(NMOvsFactoryClass *klass)
+{
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->start               = start;
+    factory_class->create_device       = create_device;
+}
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
new file mode 100644
index 00000000..da3a7989
--- /dev/null
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -0,0 +1,2658 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-ovsdb.h"
+
+#include <gmodule.h>
+#include <gio/gunixsocketaddress.h>
+
+#include "nm-glib-aux/nm-jansson.h"
+#include "nm-glib-aux/nm-str-buf.h"
+#include "nm-core-utils.h"
+#include "nm-core-internal.h"
+#include "devices/nm-device.h"
+#include "nm-manager.h"
+#include "nm-setting-ovs-external-ids.h"
+
+/*****************************************************************************/
+
+#define OVSDB_MAX_FAILURES 3
+
+/*****************************************************************************/
+
+#if JANSSON_VERSION_HEX < 0x020400
+    #warning "requires at least libjansson 2.4"
+#endif
+
+typedef struct {
+    char *     port_uuid;
+    char *     name;
+    char *     connection_uuid;
+    GPtrArray *interfaces; /* interface uuids */
+    GArray *   external_ids;
+} OpenvswitchPort;
+
+typedef struct {
+    char *     bridge_uuid;
+    char *     name;
+    char *     connection_uuid;
+    GPtrArray *ports; /* port uuids */
+    GArray *   external_ids;
+} OpenvswitchBridge;
+
+typedef struct {
+    char *  interface_uuid;
+    char *  name;
+    char *  type;
+    char *  connection_uuid;
+    GArray *external_ids;
+} OpenvswitchInterface;
+
+/*****************************************************************************/
+
+typedef void (*OvsdbMethodCallback)(NMOvsdb *self,
+                                    json_t * response,
+                                    GError * error,
+                                    gpointer user_data);
+
+typedef enum {
+    OVSDB_MONITOR,
+    OVSDB_ADD_INTERFACE,
+    OVSDB_DEL_INTERFACE,
+    OVSDB_SET_INTERFACE_MTU,
+    OVSDB_SET_EXTERNAL_IDS,
+} OvsdbCommand;
+
+#define CALL_ID_UNSPEC G_MAXUINT64
+
+typedef union {
+    struct {
+    } monitor;
+    struct {
+        NMConnection *bridge;
+        NMConnection *port;
+        NMConnection *interface;
+        NMDevice *    bridge_device;
+        NMDevice *    interface_device;
+    } add_interface;
+    struct {
+        char *ifname;
+    } del_interface;
+    struct {
+        char *  ifname;
+        guint32 mtu;
+    } set_interface_mtu;
+    struct {
+        NMDeviceType device_type;
+        char *       ifname;
+        char *       connection_uuid;
+        GHashTable * exid_old;
+        GHashTable * exid_new;
+    } set_external_ids;
+} OvsdbMethodPayload;
+
+typedef struct {
+    NMOvsdb *           self;
+    CList               calls_lst;
+    guint64             call_id;
+    OvsdbCommand        command;
+    OvsdbMethodCallback callback;
+    gpointer            user_data;
+    OvsdbMethodPayload  payload;
+} OvsdbMethodCall;
+
+/*****************************************************************************/
+
+enum {
+    DEVICE_ADDED,
+    DEVICE_REMOVED,
+    INTERFACE_FAILED,
+    READY,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    GSocketClient *    client;
+    GSocketConnection *conn;
+    GCancellable *     cancellable;
+    char               buf[4096]; /* Input buffer */
+    size_t             bufp;      /* Last decoded byte in the input buffer. */
+    GString *          input;     /* JSON stream waiting for decoding. */
+    GString *          output;    /* JSON stream to be sent. */
+    guint64            call_id_counter;
+
+    CList calls_lst_head;
+
+    GHashTable *interfaces; /* interface uuid => OpenvswitchInterface */
+    GHashTable *ports;      /* port uuid => OpenvswitchPort */
+    GHashTable *bridges;    /* bridge uuid => OpenvswitchBridge */
+    char *      db_uuid;
+    guint       num_failures;
+    guint       num_pending_deletions;
+    bool        ready : 1;
+} NMOvsdbPrivate;
+
+struct _NMOvsdb {
+    GObject        parent;
+    NMOvsdbPrivate _priv;
+};
+
+struct _NMOvsdbClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMOvsdb, nm_ovsdb, G_TYPE_OBJECT)
+
+#define NM_OVSDB_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMOvsdb, NM_IS_OVSDB)
+
+NM_DEFINE_SINGLETON_GETTER(NMOvsdb, nm_ovsdb_get, NM_TYPE_OVSDB);
+
+/*****************************************************************************/
+
+static void ovsdb_try_connect(NMOvsdb *self);
+static void ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing);
+static void ovsdb_read(NMOvsdb *self);
+static void ovsdb_write(NMOvsdb *self);
+static void ovsdb_next_command(NMOvsdb *self);
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_DEVICE
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "ovsdb", __VA_ARGS__)
+
+#define _NMLOG_call(level, call, ...)                                                  \
+    _NMLOG((level),                                                                    \
+           "call[" NM_HASH_OBFUSCATE_PTR_FMT "]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+           NM_HASH_OBFUSCATE_PTR((call)) _NM_UTILS_MACRO_REST(__VA_ARGS__))
+
+#define _LOGT_call(call, ...) _NMLOG_call(LOGL_TRACE, (call), __VA_ARGS__)
+
+/*****************************************************************************/
+
+#define OVSDB_METHOD_PAYLOAD_MONITOR() \
+    (&((const OvsdbMethodPayload){     \
+        .monitor = {},                 \
+    }))
+
+#define OVSDB_METHOD_PAYLOAD_ADD_INTERFACE(xbridge,           \
+                                           xport,             \
+                                           xinterface,        \
+                                           xbridge_device,    \
+                                           xinterface_device) \
+    (&((const OvsdbMethodPayload){                            \
+        .add_interface =                                      \
+            {                                                 \
+                .bridge           = (xbridge),                \
+                .port             = (xport),                  \
+                .interface        = (xinterface),             \
+                .bridge_device    = (xbridge_device),         \
+                .interface_device = (xinterface_device),      \
+            },                                                \
+    }))
+
+#define OVSDB_METHOD_PAYLOAD_DEL_INTERFACE(xifname)               \
+    (&((const OvsdbMethodPayload){                                \
+        .del_interface =                                          \
+            {                                                     \
+                .ifname = (char *) NM_CONSTCAST(char, (xifname)), \
+            },                                                    \
+    }))
+
+#define OVSDB_METHOD_PAYLOAD_SET_INTERFACE_MTU(xifname, xmtu)     \
+    (&((const OvsdbMethodPayload){                                \
+        .set_interface_mtu =                                      \
+            {                                                     \
+                .ifname = (char *) NM_CONSTCAST(char, (xifname)), \
+                .mtu    = (xmtu),                                 \
+            },                                                    \
+    }))
+
+#define OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(xdevice_type,                         \
+                                              xifname,                              \
+                                              xconnection_uuid,                     \
+                                              xexid_old,                            \
+                                              xexid_new)                            \
+    (&((const OvsdbMethodPayload){                                                  \
+        .set_external_ids =                                                         \
+            {                                                                       \
+                .device_type     = xdevice_type,                                    \
+                .ifname          = (char *) NM_CONSTCAST(char, (xifname)),          \
+                .connection_uuid = (char *) NM_CONSTCAST(char, (xconnection_uuid)), \
+                .exid_old        = (xexid_old),                                     \
+                .exid_new        = (xexid_new),                                     \
+            },                                                                      \
+    }))
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(_device_type_to_table,
+                                  NMDeviceType,
+                                  NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_BRIDGE, "Bridge"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_PORT, "Port"),
+                                  NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_TYPE_OVS_INTERFACE,
+                                                           "Interface"),
+                                  NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER(), );
+
+/*****************************************************************************/
+
+static void
+_call_complete(OvsdbMethodCall *call, json_t *response, GError *error)
+{
+    if (response) {
+        gs_free char *str = NULL;
+
+        str = json_dumps(response, 0);
+        if (error)
+            _LOGT_call(call, "completed: %s ; error: %s", str, error->message);
+        else
+            _LOGT_call(call, "completed: %s", str);
+    } else {
+        nm_assert(error);
+        _LOGT_call(call, "completed: error: %s", error->message);
+    }
+
+    c_list_unlink_stale(&call->calls_lst);
+
+    if (call->callback)
+        call->callback(call->self, response, error, call->user_data);
+
+    switch (call->command) {
+    case OVSDB_MONITOR:
+        break;
+    case OVSDB_ADD_INTERFACE:
+        g_clear_object(&call->payload.add_interface.bridge);
+        g_clear_object(&call->payload.add_interface.port);
+        g_clear_object(&call->payload.add_interface.interface);
+        g_clear_object(&call->payload.add_interface.bridge_device);
+        g_clear_object(&call->payload.add_interface.interface_device);
+        break;
+    case OVSDB_DEL_INTERFACE:
+        nm_clear_g_free(&call->payload.del_interface.ifname);
+        break;
+    case OVSDB_SET_INTERFACE_MTU:
+        nm_clear_g_free(&call->payload.set_interface_mtu.ifname);
+        break;
+    case OVSDB_SET_EXTERNAL_IDS:
+        nm_clear_g_free(&call->payload.set_external_ids.ifname);
+        nm_clear_g_free(&call->payload.set_external_ids.connection_uuid);
+        nm_clear_pointer(&call->payload.set_external_ids.exid_old, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_external_ids.exid_new, g_hash_table_destroy);
+        break;
+    }
+
+    nm_g_slice_free(call);
+}
+
+/*****************************************************************************/
+
+static void
+_free_bridge(OpenvswitchBridge *ovs_bridge)
+{
+    g_free(ovs_bridge->bridge_uuid);
+    g_free(ovs_bridge->name);
+    g_free(ovs_bridge->connection_uuid);
+    g_ptr_array_free(ovs_bridge->ports, TRUE);
+    nm_g_array_unref(ovs_bridge->external_ids);
+    nm_g_slice_free(ovs_bridge);
+}
+
+static void
+_free_port(OpenvswitchPort *ovs_port)
+{
+    g_free(ovs_port->port_uuid);
+    g_free(ovs_port->name);
+    g_free(ovs_port->connection_uuid);
+    g_ptr_array_free(ovs_port->interfaces, TRUE);
+    nm_g_array_unref(ovs_port->external_ids);
+    nm_g_slice_free(ovs_port);
+}
+
+static void
+_free_interface(OpenvswitchInterface *ovs_interface)
+{
+    g_free(ovs_interface->interface_uuid);
+    g_free(ovs_interface->name);
+    g_free(ovs_interface->connection_uuid);
+    g_free(ovs_interface->type);
+    nm_g_array_unref(ovs_interface->external_ids);
+    nm_g_slice_free(ovs_interface);
+}
+
+/*****************************************************************************/
+
+static void
+_signal_emit_device_added(NMOvsdb *    self,
+                          const char * name,
+                          NMDeviceType device_type,
+                          const char * device_subtype)
+{
+    g_signal_emit(self, signals[DEVICE_ADDED], 0, name, (guint) device_type, device_subtype);
+}
+
+static void
+_signal_emit_device_removed(NMOvsdb *    self,
+                            const char * name,
+                            NMDeviceType device_type,
+                            const char * device_subtype)
+{
+    g_signal_emit(self, signals[DEVICE_REMOVED], 0, name, (guint) device_type, device_subtype);
+}
+
+static void
+_signal_emit_interface_failed(NMOvsdb *   self,
+                              const char *name,
+                              const char *connection_uuid,
+                              const char *error)
+{
+    g_signal_emit(self, signals[INTERFACE_FAILED], 0, name, connection_uuid, error);
+}
+
+/*****************************************************************************/
+
+/**
+ * ovsdb_call_method:
+ *
+ * Queues the ovsdb command. Eventually fires the command right away if
+ * there's no command pending completion.
+ */
+static void
+ovsdb_call_method(NMOvsdb *                 self,
+                  OvsdbMethodCallback       callback,
+                  gpointer                  user_data,
+                  gboolean                  add_first,
+                  OvsdbCommand              command,
+                  const OvsdbMethodPayload *payload)
+{
+    NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self);
+    OvsdbMethodCall *call;
+
+    /* Ensure we're not unsynchronized before we queue the method call. */
+    ovsdb_try_connect(self);
+
+    call  = g_slice_new(OvsdbMethodCall);
+    *call = (OvsdbMethodCall){
+        .self      = self,
+        .call_id   = CALL_ID_UNSPEC,
+        .command   = command,
+        .callback  = callback,
+        .user_data = user_data,
+    };
+
+    if (add_first)
+        c_list_link_front(&priv->calls_lst_head, &call->calls_lst);
+    else
+        c_list_link_tail(&priv->calls_lst_head, &call->calls_lst);
+
+    /* Migrate the arguments from @payload to @call->payload. Technically,
+     * this is not a plain copy, because
+     * - call->payload is not initialized (thus no need to free the previous data).
+     * - payload does not own the data. It is merely initialized using the
+     *   OVSDB_METHOD_PAYLOAD_*() macros. */
+    switch (command) {
+    case OVSDB_MONITOR:
+        _LOGT_call(call, "new: monitor");
+        break;
+    case OVSDB_ADD_INTERFACE:
+        /* FIXME(applied-connection-immutable): we should not modify the applied
+         *   connection, consequently there is no need to clone the connections. */
+        call->payload.add_interface.bridge =
+            nm_simple_connection_new_clone(payload->add_interface.bridge);
+        call->payload.add_interface.port =
+            nm_simple_connection_new_clone(payload->add_interface.port);
+        call->payload.add_interface.interface =
+            nm_simple_connection_new_clone(payload->add_interface.interface);
+        call->payload.add_interface.bridge_device =
+            g_object_ref(payload->add_interface.bridge_device);
+        call->payload.add_interface.interface_device =
+            g_object_ref(payload->add_interface.interface_device);
+        _LOGT_call(call,
+                   "new: add-interface bridge=%s port=%s interface=%s",
+                   nm_connection_get_interface_name(call->payload.add_interface.bridge),
+                   nm_connection_get_interface_name(call->payload.add_interface.port),
+                   nm_connection_get_interface_name(call->payload.add_interface.interface));
+        break;
+    case OVSDB_DEL_INTERFACE:
+        call->payload.del_interface.ifname = g_strdup(payload->del_interface.ifname);
+        _LOGT_call(call, "new: del-interface interface=%s", call->payload.del_interface.ifname);
+        break;
+    case OVSDB_SET_INTERFACE_MTU:
+        call->payload.set_interface_mtu.ifname = g_strdup(payload->set_interface_mtu.ifname);
+        call->payload.set_interface_mtu.mtu    = payload->set_interface_mtu.mtu;
+        _LOGT_call(call,
+                   "new: set-interface-mtu interface=%s mtu=%u",
+                   call->payload.set_interface_mtu.ifname,
+                   call->payload.set_interface_mtu.mtu);
+        break;
+    case OVSDB_SET_EXTERNAL_IDS:
+        call->payload.set_external_ids.device_type = payload->set_external_ids.device_type;
+        call->payload.set_external_ids.ifname      = g_strdup(payload->set_external_ids.ifname);
+        call->payload.set_external_ids.connection_uuid =
+            g_strdup(payload->set_external_ids.connection_uuid);
+        call->payload.set_external_ids.exid_old =
+            nm_g_hash_table_ref(payload->set_external_ids.exid_old);
+        call->payload.set_external_ids.exid_new =
+            nm_g_hash_table_ref(payload->set_external_ids.exid_new);
+        _LOGT_call(call,
+                   "new: set-external-ids con-uuid=%s, interface=%s",
+                   call->payload.set_external_ids.connection_uuid,
+                   call->payload.set_external_ids.ifname);
+        break;
+    }
+
+    ovsdb_next_command(self);
+}
+
+/*****************************************************************************/
+
+/* Create and process the JSON-RPC messages from ovsdb. */
+
+/**
+ * _expect_ovs_bridges:
+ *
+ * Return a command that will fail the transaction if the actual set of
+ * bridges doesn't match @bridges. This is a way of detecting race conditions
+ * with other ovsdb clients that might be adding or removing bridges
+ * at the same time.
+ */
+static void
+_expect_ovs_bridges(json_t *params, const char *db_uuid, json_t *bridges)
+{
+    json_array_append_new(
+        params,
+        json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, [s, s]]]}",
+                  "op",
+                  "wait",
+                  "table",
+                  "Open_vSwitch",
+                  "timeout",
+                  0,
+                  "columns",
+                  "bridges",
+                  "until",
+                  "==",
+                  "rows",
+                  "bridges",
+                  "set",
+                  bridges,
+                  "where",
+                  "_uuid",
+                  "==",
+                  "uuid",
+                  db_uuid));
+}
+
+/**
+ * _set_ovs_bridges:
+ *
+ * Return a command that will update the list of bridges in @db_uuid
+ * database to @new_bridges.
+ */
+static void
+_set_ovs_bridges(json_t *params, const char *db_uuid, json_t *new_bridges)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, [s, s]]]}",
+                                    "op",
+                                    "update",
+                                    "table",
+                                    "Open_vSwitch",
+                                    "row",
+                                    "bridges",
+                                    "set",
+                                    new_bridges,
+                                    "where",
+                                    "_uuid",
+                                    "==",
+                                    "uuid",
+                                    db_uuid));
+}
+
+/**
+ * _expect_bridge_ports:
+ *
+ * Return a command that will fail the transaction if the actual set of
+ * ports in bridge @ifname doesn't match @ports. This is a way of detecting
+ * race conditions with other ovsdb clients that might be adding or removing
+ * bridge ports at the same time.
+ */
+static void
+_expect_bridge_ports(json_t *params, const char *ifname, json_t *ports)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, s]]}",
+                                    "op",
+                                    "wait",
+                                    "table",
+                                    "Bridge",
+                                    "timeout",
+                                    0,
+                                    "columns",
+                                    "ports",
+                                    "until",
+                                    "==",
+                                    "rows",
+                                    "ports",
+                                    "set",
+                                    ports,
+                                    "where",
+                                    "name",
+                                    "==",
+                                    ifname));
+}
+
+/**
+ * _set_bridge_ports:
+ *
+ * Return a command that will update the list of ports of bridge
+ * @ifname to @new_ports.
+ */
+static void
+_set_bridge_ports(json_t *params, const char *ifname, json_t *new_ports)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, s]]}",
+                                    "op",
+                                    "update",
+                                    "table",
+                                    "Bridge",
+                                    "row",
+                                    "ports",
+                                    "set",
+                                    new_ports,
+                                    "where",
+                                    "name",
+                                    "==",
+                                    ifname));
+}
+
+static void
+_set_bridge_mac(json_t *params, const char *ifname, const char *mac)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:{s:[s, [[s, s]]]}, s:[[s, s, s]]}",
+                                    "op",
+                                    "update",
+                                    "table",
+                                    "Bridge",
+                                    "row",
+                                    "other_config",
+                                    "map",
+                                    "hwaddr",
+                                    mac,
+                                    "where",
+                                    "name",
+                                    "==",
+                                    ifname));
+}
+
+/**
+ * _expect_port_interfaces:
+ *
+ * Return a command that will fail the transaction if the actual set of
+ * interfaces in port @ifname doesn't match @interfaces. This is a way of
+ * detecting race conditions with other ovsdb clients that might be adding
+ * or removing port interfaces at the same time.
+ */
+static void
+_expect_port_interfaces(json_t *params, const char *ifname, json_t *interfaces)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:i, s:[s], s:s, s:[{s:[s, O]}], s:[[s, s, s]]}",
+                                    "op",
+                                    "wait",
+                                    "table",
+                                    "Port",
+                                    "timeout",
+                                    0,
+                                    "columns",
+                                    "interfaces",
+                                    "until",
+                                    "==",
+                                    "rows",
+                                    "interfaces",
+                                    "set",
+                                    interfaces,
+                                    "where",
+                                    "name",
+                                    "==",
+                                    ifname));
+}
+
+/**
+ * _set_port_interfaces:
+ *
+ * Return a command that will update the list of interfaces of port @ifname
+ * to @new_interfaces.
+ */
+static void
+_set_port_interfaces(json_t *params, const char *ifname, json_t *new_interfaces)
+{
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:{s:[s, O]}, s:[[s, s, s]]}",
+                                    "op",
+                                    "update",
+                                    "table",
+                                    "Port",
+                                    "row",
+                                    "interfaces",
+                                    "set",
+                                    new_interfaces,
+                                    "where",
+                                    "name",
+                                    "==",
+                                    ifname));
+}
+
+static json_t *
+_j_create_external_ids_array_new(NMConnection *connection)
+{
+    json_t *                 array;
+    const char *const *      external_ids   = NULL;
+    guint                    n_external_ids = 0;
+    guint                    i;
+    const char *             uuid;
+    NMSettingOvsExternalIDs *s_exid;
+
+    nm_assert(NM_IS_CONNECTION(connection));
+
+    array = json_array();
+
+    uuid = nm_connection_get_uuid(connection);
+    nm_assert(uuid);
+    json_array_append_new(array, json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, uuid));
+
+    s_exid = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_EXTERNAL_IDS);
+    if (s_exid)
+        external_ids = nm_setting_ovs_external_ids_get_data_keys(s_exid, &n_external_ids);
+    for (i = 0; i < n_external_ids; i++) {
+        const char *k = external_ids[i];
+
+        json_array_append_new(
+            array,
+            json_pack("[s, s]", k, nm_setting_ovs_external_ids_get_data(s_exid, k)));
+    }
+
+    return json_pack("[s, o]", "map", array);
+}
+
+static json_t *
+_j_create_external_ids_array_update(const char *connection_uuid,
+                                    GHashTable *exid_old,
+                                    GHashTable *exid_new)
+{
+    GHashTableIter iter;
+    json_t *       mutations;
+    json_t *       array;
+    const char *   key;
+    const char *   val;
+
+    nm_assert(connection_uuid);
+
+    mutations = json_array();
+
+    if (exid_old) {
+        array = NULL;
+        g_hash_table_iter_init(&iter, exid_old);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) {
+            if (nm_g_hash_table_contains(exid_new, key))
+                continue;
+            if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+                continue;
+
+            if (!array)
+                array = json_array();
+
+            json_array_append_new(array, json_string(key));
+        }
+        if (array) {
+            json_array_append_new(
+                mutations,
+                json_pack("[s, s, [s, o]]", "external_ids", "delete", "set", array));
+        }
+    }
+
+    array = json_array();
+
+    json_array_append_new(
+        array,
+        json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, connection_uuid));
+
+    if (exid_new) {
+        g_hash_table_iter_init(&iter, exid_new);
+        while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val)) {
+            if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+                continue;
+            json_array_append_new(array, json_pack("[s, s]", key, val));
+        }
+    }
+
+    json_array_append_new(mutations,
+                          json_pack("[s, s, [s, o]]", "external_ids", "insert", "map", array));
+    return mutations;
+}
+
+/**
+ * _insert_interface:
+ *
+ * Returns an commands that adds new interface from a given connection.
+ */
+static void
+_insert_interface(json_t *      params,
+                  NMConnection *interface,
+                  NMDevice *    interface_device,
+                  const char *  cloned_mac)
+{
+    const char *           type = NULL;
+    NMSettingOvsInterface *s_ovs_iface;
+    NMSettingOvsDpdk *     s_ovs_dpdk;
+    NMSettingOvsPatch *    s_ovs_patch;
+    json_t *               options = json_array();
+    json_t *               row;
+    guint32                mtu = 0;
+
+    s_ovs_iface = nm_connection_get_setting_ovs_interface(interface);
+    if (s_ovs_iface)
+        type = nm_setting_ovs_interface_get_interface_type(s_ovs_iface);
+
+    if (nm_streq0(type, "internal")) {
+        NMSettingWired *s_wired;
+
+        s_wired = _nm_connection_get_setting(interface, NM_TYPE_SETTING_WIRED);
+        if (s_wired)
+            mtu = nm_setting_wired_get_mtu(s_wired);
+    }
+
+    json_array_append_new(options, json_string("map"));
+
+    s_ovs_dpdk =
+        (NMSettingOvsDpdk *) nm_connection_get_setting(interface, NM_TYPE_SETTING_OVS_DPDK);
+    if (!s_ovs_dpdk)
+        s_ovs_patch = nm_connection_get_setting_ovs_patch(interface);
+
+    if (s_ovs_dpdk) {
+        json_array_append_new(
+            options,
+            json_pack("[[s, s]]", "dpdk-devargs", nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk)));
+    } else if (s_ovs_patch) {
+        json_array_append_new(
+            options,
+            json_pack("[[s, s]]", "peer", nm_setting_ovs_patch_get_peer(s_ovs_patch)));
+    } else {
+        json_array_append_new(options, json_array());
+    }
+
+    row = json_pack("{s:s, s:s, s:o, s:o}",
+                    "name",
+                    nm_connection_get_interface_name(interface),
+                    "type",
+                    type ?: "",
+                    "options",
+                    options,
+                    "external_ids",
+                    _j_create_external_ids_array_new(interface));
+
+    if (cloned_mac)
+        json_object_set_new(row, "mac", json_string(cloned_mac));
+
+    if (mtu != 0)
+        json_object_set_new(row, "mtu_request", json_integer(mtu));
+
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:o, s:s}",
+                                    "op",
+                                    "insert",
+                                    "table",
+                                    "Interface",
+                                    "row",
+                                    row,
+                                    "uuid-name",
+                                    "rowInterface"));
+}
+
+/**
+ * _insert_port:
+ *
+ * Returns an commands that adds new port from a given connection.
+ */
+static void
+_insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
+{
+    NMSettingOvsPort *s_ovs_port;
+    const char *      vlan_mode      = NULL;
+    guint             tag            = 0;
+    const char *      lacp           = NULL;
+    const char *      bond_mode      = NULL;
+    guint             bond_updelay   = 0;
+    guint             bond_downdelay = 0;
+    json_t *          row;
+
+    s_ovs_port = nm_connection_get_setting_ovs_port(port);
+
+    row = json_object();
+
+    if (s_ovs_port) {
+        vlan_mode      = nm_setting_ovs_port_get_vlan_mode(s_ovs_port);
+        tag            = nm_setting_ovs_port_get_tag(s_ovs_port);
+        lacp           = nm_setting_ovs_port_get_lacp(s_ovs_port);
+        bond_mode      = nm_setting_ovs_port_get_bond_mode(s_ovs_port);
+        bond_updelay   = nm_setting_ovs_port_get_bond_updelay(s_ovs_port);
+        bond_downdelay = nm_setting_ovs_port_get_bond_downdelay(s_ovs_port);
+    }
+
+    if (vlan_mode)
+        json_object_set_new(row, "vlan_mode", json_string(vlan_mode));
+    if (tag)
+        json_object_set_new(row, "tag", json_integer(tag));
+    if (lacp)
+        json_object_set_new(row, "lacp", json_string(lacp));
+    if (bond_mode)
+        json_object_set_new(row, "bond_mode", json_string(bond_mode));
+    if (bond_updelay)
+        json_object_set_new(row, "bond_updelay", json_integer(bond_updelay));
+    if (bond_downdelay)
+        json_object_set_new(row, "bond_downdelay", json_integer(bond_downdelay));
+
+    json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(port)));
+    json_object_set_new(row, "interfaces", json_pack("[s, O]", "set", new_interfaces));
+    json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(port));
+
+    /* Create a new one. */
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:o, s:s}",
+                                    "op",
+                                    "insert",
+                                    "table",
+                                    "Port",
+                                    "row",
+                                    row,
+                                    "uuid-name",
+                                    "rowPort"));
+}
+
+/**
+ * _insert_bridge:
+ *
+ * Returns an commands that adds new bridge from a given connection.
+ */
+static void
+_insert_bridge(json_t *      params,
+               NMConnection *bridge,
+               NMDevice *    bridge_device,
+               json_t *      new_ports,
+               const char *  cloned_mac)
+{
+    NMSettingOvsBridge *s_ovs_bridge;
+    const char *        fail_mode             = NULL;
+    gboolean            mcast_snooping_enable = FALSE;
+    gboolean            rstp_enable           = FALSE;
+    gboolean            stp_enable            = FALSE;
+    const char *        datapath_type         = NULL;
+    json_t *            row;
+
+    s_ovs_bridge = nm_connection_get_setting_ovs_bridge(bridge);
+
+    row = json_object();
+
+    if (s_ovs_bridge) {
+        fail_mode             = nm_setting_ovs_bridge_get_fail_mode(s_ovs_bridge);
+        mcast_snooping_enable = nm_setting_ovs_bridge_get_mcast_snooping_enable(s_ovs_bridge);
+        rstp_enable           = nm_setting_ovs_bridge_get_rstp_enable(s_ovs_bridge);
+        stp_enable            = nm_setting_ovs_bridge_get_stp_enable(s_ovs_bridge);
+        datapath_type         = nm_setting_ovs_bridge_get_datapath_type(s_ovs_bridge);
+    }
+
+    if (fail_mode)
+        json_object_set_new(row, "fail_mode", json_string(fail_mode));
+    if (mcast_snooping_enable)
+        json_object_set_new(row, "mcast_snooping_enable", json_boolean(mcast_snooping_enable));
+    if (rstp_enable)
+        json_object_set_new(row, "rstp_enable", json_boolean(rstp_enable));
+    if (stp_enable)
+        json_object_set_new(row, "stp_enable", json_boolean(stp_enable));
+    if (datapath_type)
+        json_object_set_new(row, "datapath_type", json_string(datapath_type));
+
+    json_object_set_new(row, "name", json_string(nm_connection_get_interface_name(bridge)));
+    json_object_set_new(row, "ports", json_pack("[s, O]", "set", new_ports));
+    json_object_set_new(row, "external_ids", _j_create_external_ids_array_new(bridge));
+
+    if (cloned_mac) {
+        json_object_set_new(row,
+                            "other_config",
+                            json_pack("[s, [[s, s]]]", "map", "hwaddr", cloned_mac));
+    }
+
+    /* Create a new one. */
+    json_array_append_new(params,
+                          json_pack("{s:s, s:s, s:o, s:s}",
+                                    "op",
+                                    "insert",
+                                    "table",
+                                    "Bridge",
+                                    "row",
+                                    row,
+                                    "uuid-name",
+                                    "rowBridge"));
+}
+
+/**
+ * _inc_next_cfg:
+ *
+ * Returns an mutate command that bumps next_cfg upon successful completion
+ * of the transaction it is in.
+ */
+static json_t *
+_inc_next_cfg(const char *db_uuid)
+{
+    return json_pack("{s:s, s:s, s:[[s, s, i]], s:[[s, s, [s, s]]]}",
+                     "op",
+                     "mutate",
+                     "table",
+                     "Open_vSwitch",
+                     "mutations",
+                     "next_cfg",
+                     "+=",
+                     1,
+                     "where",
+                     "_uuid",
+                     "==",
+                     "uuid",
+                     db_uuid);
+}
+
+/**
+ * _add_interface:
+ *
+ * Adds an interface as specified by @interface connection, optionally creating
+ * a parent @port and @bridge if needed.
+ */
+static void
+_add_interface(NMOvsdb *     self,
+               json_t *      params,
+               NMConnection *bridge,
+               NMConnection *port,
+               NMConnection *interface,
+               NMDevice *    bridge_device,
+               NMDevice *    interface_device)
+{
+    NMOvsdbPrivate *      priv = NM_OVSDB_GET_PRIVATE(self);
+    GHashTableIter        iter;
+    const char *          port_uuid;
+    const char *          interface_uuid;
+    const char *          bridge_name;
+    const char *          port_name;
+    const char *          interface_name;
+    OpenvswitchBridge *   ovs_bridge           = NULL;
+    OpenvswitchPort *     ovs_port             = NULL;
+    OpenvswitchInterface *ovs_interface        = NULL;
+    nm_auto_decref_json json_t *bridges        = NULL;
+    nm_auto_decref_json json_t *new_bridges    = NULL;
+    nm_auto_decref_json json_t *ports          = NULL;
+    nm_auto_decref_json json_t *new_ports      = NULL;
+    nm_auto_decref_json json_t *interfaces     = NULL;
+    nm_auto_decref_json json_t *new_interfaces = NULL;
+    gboolean                    has_interface  = FALSE;
+    gboolean                    interface_is_local;
+    gs_free char *              bridge_cloned_mac    = NULL;
+    gs_free char *              interface_cloned_mac = NULL;
+    GError *                    error                = NULL;
+    int                         pi;
+    int                         ii;
+
+    bridges        = json_array();
+    ports          = json_array();
+    interfaces     = json_array();
+    new_bridges    = json_array();
+    new_ports      = json_array();
+    new_interfaces = json_array();
+
+    bridge_name        = nm_connection_get_interface_name(bridge);
+    port_name          = nm_connection_get_interface_name(port);
+    interface_name     = nm_connection_get_interface_name(interface);
+    interface_is_local = nm_streq0(bridge_name, interface_name);
+
+    /* Determine cloned MAC addresses */
+    if (!nm_device_hw_addr_get_cloned(bridge_device,
+                                      bridge,
+                                      FALSE,
+                                      &bridge_cloned_mac,
+                                      NULL,
+                                      &error)) {
+        _LOGW("Cannot determine cloned MAC for OVS %s '%s': %s",
+              "bridge",
+              bridge_name,
+              error->message);
+        g_clear_error(&error);
+    }
+
+    if (!nm_device_hw_addr_get_cloned(interface_device,
+                                      interface,
+                                      FALSE,
+                                      &interface_cloned_mac,
+                                      NULL,
+                                      &error)) {
+        _LOGW("Cannot determine cloned MAC for OVS %s '%s': %s",
+              "interface",
+              interface_name,
+              error->message);
+        g_clear_error(&error);
+    }
+
+    /* For local interfaces, ovs complains if it finds a
+     * MAC address in the Interface table because it only takes
+     * the MAC from the Bridge table.
+     * Set any cloned MAC present in a local interface connection
+     * into the Bridge table, unless conflicting with the bridge MAC. */
+    if (interface_is_local && interface_cloned_mac) {
+        if (bridge_cloned_mac && !nm_streq(interface_cloned_mac, bridge_cloned_mac)) {
+            _LOGW("Cloned MAC '%s' of local ovs-interface '%s' conflicts with MAC '%s' of bridge "
+                  "'%s'",
+                  interface_cloned_mac,
+                  interface_name,
+                  bridge_cloned_mac,
+                  bridge_name);
+            nm_clear_g_free(&interface_cloned_mac);
+        } else {
+            nm_clear_g_free(&bridge_cloned_mac);
+            bridge_cloned_mac = g_steal_pointer(&interface_cloned_mac);
+            _LOGT("'%s' is a local ovs-interface, the MAC will be set on ovs-bridge '%s'",
+                  interface_name,
+                  bridge_name);
+        }
+    }
+
+    g_hash_table_iter_init(&iter, priv->bridges);
+    while (g_hash_table_iter_next(&iter, (gpointer) &ovs_bridge, NULL)) {
+        json_array_append_new(bridges, json_pack("[s, s]", "uuid", ovs_bridge->bridge_uuid));
+
+        if (!nm_streq0(ovs_bridge->name, bridge_name)
+            || !nm_streq0(ovs_bridge->connection_uuid, nm_connection_get_uuid(bridge)))
+            continue;
+
+        for (pi = 0; pi < ovs_bridge->ports->len; pi++) {
+            port_uuid = g_ptr_array_index(ovs_bridge->ports, pi);
+            ovs_port  = g_hash_table_lookup(priv->ports, &port_uuid);
+
+            json_array_append_new(ports, json_pack("[s, s]", "uuid", port_uuid));
+
+            if (!ovs_port) {
+                /* This would be a violation of ovsdb's reference integrity (a bug). */
+                _LOGW("Unknown port '%s' in bridge '%s'", port_uuid, ovs_bridge->bridge_uuid);
+                continue;
+            }
+
+            if (!nm_streq(ovs_port->name, port_name)
+                || !nm_streq0(ovs_port->connection_uuid, nm_connection_get_uuid(port)))
+                continue;
+
+            for (ii = 0; ii < ovs_port->interfaces->len; ii++) {
+                interface_uuid = g_ptr_array_index(ovs_port->interfaces, ii);
+                ovs_interface  = g_hash_table_lookup(priv->interfaces, &interface_uuid);
+
+                json_array_append_new(interfaces, json_pack("[s, s]", "uuid", interface_uuid));
+
+                if (!ovs_interface) {
+                    /* This would be a violation of ovsdb's reference integrity (a bug). */
+                    _LOGW("Unknown interface '%s' in port '%s'", interface_uuid, port_uuid);
+                    continue;
+                }
+                if (nm_streq(ovs_interface->name, interface_name)
+                    && nm_streq0(ovs_interface->connection_uuid, nm_connection_get_uuid(interface)))
+                    has_interface = TRUE;
+            }
+
+            break;
+        }
+
+        break;
+    }
+
+    json_array_extend(new_bridges, bridges);
+    json_array_extend(new_ports, ports);
+    json_array_extend(new_interfaces, interfaces);
+
+    if (json_array_size(interfaces) == 0) {
+        /* Need to create a port. */
+        if (json_array_size(ports) == 0) {
+            /* Need to create a bridge. */
+            _expect_ovs_bridges(params, priv->db_uuid, bridges);
+            json_array_append_new(new_bridges, json_pack("[s, s]", "named-uuid", "rowBridge"));
+            _set_ovs_bridges(params, priv->db_uuid, new_bridges);
+            _insert_bridge(params, bridge, bridge_device, new_ports, bridge_cloned_mac);
+        } else {
+            /* Bridge already exists. */
+            g_return_if_fail(ovs_bridge);
+            _expect_bridge_ports(params, ovs_bridge->name, ports);
+            _set_bridge_ports(params, bridge_name, new_ports);
+            if (bridge_cloned_mac && interface_is_local)
+                _set_bridge_mac(params, bridge_name, bridge_cloned_mac);
+        }
+
+        json_array_append_new(new_ports, json_pack("[s, s]", "named-uuid", "rowPort"));
+        _insert_port(params, port, new_interfaces);
+    } else {
+        /* Port already exists */
+        g_return_if_fail(ovs_port);
+        _expect_port_interfaces(params, ovs_port->name, interfaces);
+        _set_port_interfaces(params, port_name, new_interfaces);
+    }
+
+    if (!has_interface) {
+        _insert_interface(params, interface, interface_device, interface_cloned_mac);
+        json_array_append_new(new_interfaces, json_pack("[s, s]", "named-uuid", "rowInterface"));
+    }
+}
+
+/**
+ * _delete_interface:
+ *
+ * Removes an interface of @ifname name, collecting empty ports and bridge
+ * if last item is removed from them.
+ */
+static void
+_delete_interface(NMOvsdb *self, json_t *params, const char *ifname)
+{
+    NMOvsdbPrivate *      priv = NM_OVSDB_GET_PRIVATE(self);
+    GHashTableIter        iter;
+    char *                port_uuid;
+    char *                interface_uuid;
+    OpenvswitchBridge *   ovs_bridge;
+    OpenvswitchPort *     ovs_port;
+    OpenvswitchInterface *ovs_interface;
+    nm_auto_decref_json json_t *bridges     = NULL;
+    nm_auto_decref_json json_t *new_bridges = NULL;
+    gboolean                    bridges_changed;
+    gboolean                    ports_changed;
+    gboolean                    interfaces_changed;
+    int                         pi;
+    int                         ii;
+
+    bridges         = json_array();
+    new_bridges     = json_array();
+    bridges_changed = FALSE;
+
+    g_hash_table_iter_init(&iter, priv->bridges);
+    while (g_hash_table_iter_next(&iter, (gpointer) &ovs_bridge, NULL)) {
+        nm_auto_decref_json json_t *ports     = NULL;
+        nm_auto_decref_json json_t *new_ports = NULL;
+
+        ports         = json_array();
+        new_ports     = json_array();
+        ports_changed = FALSE;
+
+        json_array_append_new(bridges, json_pack("[s,s]", "uuid", ovs_bridge->bridge_uuid));
+
+        for (pi = 0; pi < ovs_bridge->ports->len; pi++) {
+            nm_auto_decref_json json_t *interfaces     = NULL;
+            nm_auto_decref_json json_t *new_interfaces = NULL;
+
+            interfaces     = json_array();
+            new_interfaces = json_array();
+            port_uuid      = g_ptr_array_index(ovs_bridge->ports, pi);
+            ovs_port       = g_hash_table_lookup(priv->ports, &port_uuid);
+
+            json_array_append_new(ports, json_pack("[s,s]", "uuid", port_uuid));
+
+            interfaces_changed = FALSE;
+
+            if (!ovs_port) {
+                /* This would be a violation of ovsdb's reference integrity (a bug). */
+                _LOGW("Unknown port '%s' in bridge '%s'", port_uuid, ovs_bridge->bridge_uuid);
+                continue;
+            }
+
+            for (ii = 0; ii < ovs_port->interfaces->len; ii++) {
+                interface_uuid = g_ptr_array_index(ovs_port->interfaces, ii);
+                ovs_interface  = g_hash_table_lookup(priv->interfaces, &interface_uuid);
+
+                json_array_append_new(interfaces, json_pack("[s,s]", "uuid", interface_uuid));
+
+                if (ovs_interface) {
+                    if (nm_streq(ovs_interface->name, ifname)) {
+                        /* skip the interface */
+                        interfaces_changed = TRUE;
+                        continue;
+                    }
+                } else {
+                    /* This would be a violation of ovsdb's reference integrity (a bug). */
+                    _LOGW("Unknown interface '%s' in port '%s'", interface_uuid, port_uuid);
+                }
+
+                json_array_append_new(new_interfaces, json_pack("[s,s]", "uuid", interface_uuid));
+            }
+
+            if (json_array_size(new_interfaces) == 0) {
+                ports_changed = TRUE;
+            } else {
+                if (interfaces_changed) {
+                    _expect_port_interfaces(params, ovs_port->name, interfaces);
+                    _set_port_interfaces(params, ovs_port->name, new_interfaces);
+                }
+                json_array_append_new(new_ports, json_pack("[s,s]", "uuid", port_uuid));
+            }
+        }
+
+        if (json_array_size(new_ports) == 0) {
+            bridges_changed = TRUE;
+        } else {
+            if (ports_changed) {
+                _expect_bridge_ports(params, ovs_bridge->name, ports);
+                _set_bridge_ports(params, ovs_bridge->name, new_ports);
+            }
+            json_array_append_new(new_bridges, json_pack("[s,s]", "uuid", ovs_bridge->bridge_uuid));
+        }
+    }
+
+    if (bridges_changed) {
+        _expect_ovs_bridges(params, priv->db_uuid, bridges);
+        _set_ovs_bridges(params, priv->db_uuid, new_bridges);
+    }
+}
+
+/**
+ * ovsdb_next_command:
+ *
+ * Translates a higher level operation (add/remove bridge/port) to a RFC 7047
+ * command serialized into JSON ands sends it over to the database.
+
+ * Only called when no command is waiting for a response, since the serialized
+ * command might depend on result of a previous one (add and remove need to
+ * include an up to date bridge list in their transactions to rule out races).
+ */
+static void
+ovsdb_next_command(NMOvsdb *self)
+{
+    NMOvsdbPrivate *    priv = NM_OVSDB_GET_PRIVATE(self);
+    OvsdbMethodCall *   call;
+    char *              cmd;
+    nm_auto_decref_json json_t *msg = NULL;
+
+    if (!priv->conn)
+        return;
+
+    if (c_list_is_empty(&priv->calls_lst_head))
+        return;
+
+    call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst);
+    if (call->call_id != CALL_ID_UNSPEC)
+        return;
+
+    call->call_id = ++priv->call_id_counter;
+
+    switch (call->command) {
+    case OVSDB_MONITOR:
+        msg = json_pack("{s:I, s:s, s:[s, n, {"
+                        "  s:[{s:[s, s, s]}],"
+                        "  s:[{s:[s, s, s]}],"
+                        "  s:[{s:[s, s, s, s]}],"
+                        "  s:[{s:[]}]"
+                        "}]}",
+                        "id",
+                        (json_int_t) call->call_id,
+                        "method",
+                        "monitor",
+                        "params",
+                        "Open_vSwitch",
+                        "Bridge",
+                        "columns",
+                        "name",
+                        "ports",
+                        "external_ids",
+                        "Port",
+                        "columns",
+                        "name",
+                        "interfaces",
+                        "external_ids",
+                        "Interface",
+                        "columns",
+                        "name",
+                        "type",
+                        "external_ids",
+                        "error",
+                        "Open_vSwitch",
+                        "columns");
+        break;
+    default:
+    {
+        json_t *params = NULL;
+
+        params = json_array();
+        json_array_append_new(params, json_string("Open_vSwitch"));
+        json_array_append_new(params, _inc_next_cfg(priv->db_uuid));
+
+        switch (call->command) {
+        case OVSDB_ADD_INTERFACE:
+            _add_interface(self,
+                           params,
+                           call->payload.add_interface.bridge,
+                           call->payload.add_interface.port,
+                           call->payload.add_interface.interface,
+                           call->payload.add_interface.bridge_device,
+                           call->payload.add_interface.interface_device);
+            break;
+        case OVSDB_DEL_INTERFACE:
+            _delete_interface(self, params, call->payload.del_interface.ifname);
+            break;
+        case OVSDB_SET_INTERFACE_MTU:
+            json_array_append_new(params,
+                                  json_pack("{s:s, s:s, s:{s: I}, s:[[s, s, s]]}",
+                                            "op",
+                                            "update",
+                                            "table",
+                                            "Interface",
+                                            "row",
+                                            "mtu_request",
+                                            (json_int_t) call->payload.set_interface_mtu.mtu,
+                                            "where",
+                                            "name",
+                                            "==",
+                                            call->payload.set_interface_mtu.ifname));
+            break;
+        case OVSDB_SET_EXTERNAL_IDS:
+            json_array_append_new(
+                params,
+                json_pack("{s:s, s:s, s:o, s:[[s, s, s]]}",
+                          "op",
+                          "mutate",
+                          "table",
+                          _device_type_to_table(call->payload.set_external_ids.device_type),
+                          "mutations",
+                          _j_create_external_ids_array_update(
+                              call->payload.set_external_ids.connection_uuid,
+                              call->payload.set_external_ids.exid_old,
+                              call->payload.set_external_ids.exid_new),
+                          "where",
+                          "name",
+                          "==",
+                          call->payload.set_external_ids.ifname));
+            break;
+
+        default:
+            nm_assert_not_reached();
+            break;
+        }
+
+        msg = json_pack("{s:I, s:s, s:o}",
+                        "id",
+                        (json_int_t) call->call_id,
+                        "method",
+                        "transact",
+                        "params",
+                        params);
+        break;
+    }
+    }
+
+    g_return_if_fail(msg);
+
+    cmd = json_dumps(msg, 0);
+    _LOGT_call(call, "send: call-id=%" G_GUINT64_FORMAT ", %s", call->call_id, cmd);
+    g_string_append(priv->output, cmd);
+    free(cmd);
+
+    ovsdb_write(self);
+}
+
+/**
+ * _uuids_to_array:
+ *
+ * This tidies up the somewhat non-straightforward way ovsdb represents an array
+ * of UUID elements. The single element is a tuple (called <atom> in RFC7047),
+ *
+ *   [ "uuid", "aa095ffb-e1f1-0fc4-8038-82c1ea7e4797" ]
+ *
+ * while the list of multiple UUIDs are turned into a set of such tuples ("atoms"):
+ *
+ *   [ "set", [ [ "uuid", "aa095ffb-e1f1-0fc4-8038-82c1ea7e4797" ],
+ *              [ "uuid", "185c93f6-0b39-424e-8587-77d074aa7ce0" ], ... ] ]
+ */
+static void
+_uuids_to_array_inplace(GPtrArray *array, const json_t *items)
+{
+    const char *key;
+    json_t *    value;
+    size_t      index = 0;
+    json_t *    set_value;
+    size_t      set_index;
+
+    while (index < json_array_size(items)) {
+        key = json_string_value(json_array_get(items, index));
+        index++;
+        value = json_array_get(items, index);
+        index++;
+
+        if (!value || !key)
+            return;
+
+        if (nm_streq(key, "uuid")) {
+            if (json_is_string(value))
+                g_ptr_array_add(array, g_strdup(json_string_value(value)));
+            continue;
+        }
+        if (nm_streq(key, "set")) {
+            if (json_is_array(value)) {
+                json_array_foreach (value, set_index, set_value)
+                    _uuids_to_array_inplace(array, set_value);
+            }
+            continue;
+        }
+    }
+}
+
+static GPtrArray *
+_uuids_to_array(const json_t *items)
+{
+    GPtrArray *array;
+
+    array = g_ptr_array_new_with_free_func(g_free);
+    _uuids_to_array_inplace(array, items);
+    return array;
+}
+
+static void
+_external_ids_extract(json_t *external_ids, GArray **out_array, const char **out_connection_uuid)
+{
+    json_t *array;
+    json_t *value;
+    gsize   index;
+
+    nm_assert(out_array && !*out_array);
+    nm_assert(!out_connection_uuid || !*out_connection_uuid);
+
+    if (!nm_streq0("map", json_string_value(json_array_get(external_ids, 0))))
+        return;
+
+    array = json_array_get(external_ids, 1);
+
+    json_array_foreach (array, index, value) {
+        const char *       key = json_string_value(json_array_get(value, 0));
+        const char *       val = json_string_value(json_array_get(value, 1));
+        NMUtilsNamedValue *v;
+
+        if (!key || !val)
+            continue;
+
+        if (!*out_array) {
+            *out_array = g_array_new(FALSE, FALSE, sizeof(NMUtilsNamedValue));
+            g_array_set_clear_func(*out_array,
+                                   (GDestroyNotify) nm_utils_named_value_clear_with_g_free);
+        }
+
+        v  = nm_g_array_append_new(*out_array, NMUtilsNamedValue);
+        *v = (NMUtilsNamedValue){
+            .name      = g_strdup(key),
+            .value_str = g_strdup(val),
+        };
+
+        if (out_connection_uuid && nm_streq(v->name, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID)) {
+            *out_connection_uuid = v->value_str;
+            out_connection_uuid  = NULL;
+        }
+    }
+}
+
+static gboolean
+_external_ids_equal(const GArray *arr1, const GArray *arr2)
+{
+    guint n;
+    guint i;
+
+    n = nm_g_array_len(arr1);
+
+    if (n != nm_g_array_len(arr2))
+        return FALSE;
+    for (i = 0; i < n; i++) {
+        const NMUtilsNamedValue *n1 = &g_array_index(arr1, NMUtilsNamedValue, i);
+        const NMUtilsNamedValue *n2 = &g_array_index(arr2, NMUtilsNamedValue, i);
+
+        if (!nm_streq0(n1->name, n2->name))
+            return FALSE;
+        if (!nm_streq0(n1->value_str, n2->value_str))
+            return FALSE;
+    }
+    return TRUE;
+}
+
+static char *
+_external_ids_to_string(const GArray *arr)
+{
+    NMStrBuf strbuf;
+    guint    i;
+
+    if (!arr)
+        return g_strdup("empty");
+
+    nm_str_buf_init(&strbuf, NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
+    nm_str_buf_append(&strbuf, "[");
+    for (i = 0; i < arr->len; i++) {
+        const NMUtilsNamedValue *n = &g_array_index(arr, NMUtilsNamedValue, i);
+
+        if (i > 0)
+            nm_str_buf_append_c(&strbuf, ',');
+        nm_str_buf_append_printf(&strbuf, " \"%s\" = \"%s\"]", n->name, n->value_str);
+    }
+    nm_str_buf_append(&strbuf, " ]");
+
+    return nm_str_buf_finalize(&strbuf, NULL);
+}
+
+/*****************************************************************************/
+
+/**
+ * ovsdb_got_update:
+ *
+ * Called when we've got an "update" method call (we asked for it with the monitor
+ * command). We use it to maintain a consistent view of bridge list regardless of
+ * whether the changes are done by us or externally.
+ */
+static void
+ovsdb_got_update(NMOvsdb *self, json_t *msg)
+{
+    NMOvsdbPrivate *priv      = NM_OVSDB_GET_PRIVATE(self);
+    json_t *        ovs       = NULL;
+    json_t *        bridge    = NULL;
+    json_t *        port      = NULL;
+    json_t *        interface = NULL;
+    json_t *        items;
+    json_t *        external_ids;
+    json_error_t    json_error = {
+        0,
+    };
+    void *      iter;
+    const char *name;
+    const char *key;
+    const char *type;
+    json_t *    value;
+
+    if (json_unpack_ex(msg,
+                       &json_error,
+                       0,
+                       "{s?:o, s?:o, s?:o, s?:o}",
+                       "Open_vSwitch",
+                       &ovs,
+                       "Bridge",
+                       &bridge,
+                       "Port",
+                       &port,
+                       "Interface",
+                       &interface)
+        == -1) {
+        /* This doesn't really have to be an error; the key might
+         * be missing if there really are no bridges present. */
+        _LOGD("Bad update: %s", json_error.text);
+    }
+
+    if (ovs) {
+        const char *s;
+
+        iter = json_object_iter(ovs);
+        s    = json_object_iter_key(iter);
+        if (s)
+            nm_utils_strdup_reset(&priv->db_uuid, s);
+    }
+
+    json_object_foreach (interface, key, value) {
+        OpenvswitchInterface *ovs_interface;
+        gs_unref_array GArray *external_ids_arr = NULL;
+        const char *           connection_uuid  = NULL;
+        json_t *               error            = NULL;
+        int                    r;
+
+        r = json_unpack(value,
+                        "{s:{s:s, s:s, s?:o, s:o}}",
+                        "new",
+                        "name",
+                        &name,
+                        "type",
+                        &type,
+                        "error",
+                        &error,
+                        "external_ids",
+                        &external_ids);
+        if (r != 0) {
+            gpointer unused;
+
+            r = json_unpack(value, "{s:{}}", "old");
+            if (r != 0)
+                continue;
+
+            if (!g_hash_table_steal_extended(priv->interfaces,
+                                             &key,
+                                             (gpointer *) &ovs_interface,
+                                             &unused))
+                continue;
+
+            _LOGT("obj[iface:%s]: removed an '%s' interface: %s%s%s",
+                  key,
+                  ovs_interface->type,
+                  ovs_interface->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                       ", ",
+                                       ovs_interface->connection_uuid,
+                                       ""));
+            _signal_emit_device_removed(self,
+                                        ovs_interface->name,
+                                        NM_DEVICE_TYPE_OVS_INTERFACE,
+                                        ovs_interface->type);
+            _free_interface(ovs_interface);
+            continue;
+        }
+
+        ovs_interface = g_hash_table_lookup(priv->interfaces, &key);
+
+        if (ovs_interface
+            && (!nm_streq0(ovs_interface->name, name) || !nm_streq0(ovs_interface->type, type))) {
+            if (!g_hash_table_steal(priv->interfaces, ovs_interface))
+                nm_assert_not_reached();
+            _signal_emit_device_removed(self,
+                                        ovs_interface->name,
+                                        NM_DEVICE_TYPE_OVS_INTERFACE,
+                                        ovs_interface->type);
+            nm_clear_pointer(&ovs_interface, _free_interface);
+        }
+
+        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+
+        if (ovs_interface) {
+            gboolean changed = FALSE;
+
+            nm_assert(nm_streq0(ovs_interface->name, name));
+
+            changed |= nm_utils_strdup_reset(&ovs_interface->type, type);
+            changed |= nm_utils_strdup_reset(&ovs_interface->connection_uuid, connection_uuid);
+            if (!_external_ids_equal(ovs_interface->external_ids, external_ids_arr)) {
+                NM_SWAP(&ovs_interface->external_ids, &external_ids_arr);
+                changed = TRUE;
+            }
+            if (changed) {
+                gs_free char *strtmp = NULL;
+
+                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s",
+                      key,
+                      type,
+                      ovs_interface->name,
+                      NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                           ", ",
+                                           ovs_interface->connection_uuid,
+                                           ""),
+                      (strtmp = _external_ids_to_string(ovs_interface->external_ids)));
+            }
+        } else {
+            gs_free char *strtmp = NULL;
+
+            ovs_interface  = g_slice_new(OpenvswitchInterface);
+            *ovs_interface = (OpenvswitchInterface){
+                .interface_uuid  = g_strdup(key),
+                .name            = g_strdup(name),
+                .type            = g_strdup(type),
+                .connection_uuid = g_strdup(connection_uuid),
+                .external_ids    = g_steal_pointer(&external_ids_arr),
+            };
+            g_hash_table_add(priv->interfaces, ovs_interface);
+            _LOGT("obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s",
+                  key,
+                  ovs_interface->type,
+                  ovs_interface->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                       ", ",
+                                       ovs_interface->connection_uuid,
+                                       ""),
+                  (strtmp = _external_ids_to_string(ovs_interface->external_ids)));
+            _signal_emit_device_added(self,
+                                      ovs_interface->name,
+                                      NM_DEVICE_TYPE_OVS_INTERFACE,
+                                      ovs_interface->type);
+        }
+
+        /* The error is a string. No error is indicated by an empty set,
+         * Why not: [ "set": [] ] ? */
+        if (error && json_is_string(error)) {
+            _signal_emit_interface_failed(self,
+                                          ovs_interface->name,
+                                          ovs_interface->connection_uuid,
+                                          json_string_value(error));
+        }
+    }
+
+    json_object_foreach (port, key, value) {
+        gs_unref_ptrarray GPtrArray *interfaces = NULL;
+        OpenvswitchPort *            ovs_port;
+        gs_unref_array GArray *external_ids_arr = NULL;
+        const char *           connection_uuid  = NULL;
+        int                    r;
+
+        r = json_unpack(value,
+                        "{s:{s:s, s:o, s:o}}",
+                        "new",
+                        "name",
+                        &name,
+                        "external_ids",
+                        &external_ids,
+                        "interfaces",
+                        &items);
+        if (r != 0) {
+            gpointer unused;
+
+            r = json_unpack(value, "{s:{}}", "old");
+            if (r != 0)
+                continue;
+
+            if (!g_hash_table_steal_extended(priv->ports, &key, (gpointer *) &ovs_port, &unused))
+                continue;
+
+            _LOGT("obj[port:%s]: removed a port: %s%s%s",
+                  key,
+                  ovs_port->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
+                                       ", ",
+                                       ovs_port->connection_uuid,
+                                       ""));
+            _signal_emit_device_removed(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
+            _free_port(ovs_port);
+            continue;
+        }
+
+        ovs_port = g_hash_table_lookup(priv->ports, &key);
+
+        if (ovs_port && !nm_streq0(ovs_port->name, name)) {
+            if (!g_hash_table_steal(priv->ports, ovs_port))
+                nm_assert_not_reached();
+            _signal_emit_device_removed(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
+            nm_clear_pointer(&ovs_port, _free_port);
+        }
+
+        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        interfaces = _uuids_to_array(items);
+
+        if (ovs_port) {
+            gboolean changed = FALSE;
+
+            nm_assert(nm_streq0(ovs_port->name, name));
+
+            changed |= nm_utils_strdup_reset(&ovs_port->name, name);
+            changed |= nm_utils_strdup_reset(&ovs_port->connection_uuid, connection_uuid);
+            if (nm_strv_ptrarray_cmp(ovs_port->interfaces, interfaces) != 0) {
+                NM_SWAP(&ovs_port->interfaces, &interfaces);
+                changed = TRUE;
+            }
+            if (!_external_ids_equal(ovs_port->external_ids, external_ids_arr)) {
+                NM_SWAP(&ovs_port->external_ids, &external_ids_arr);
+                changed = TRUE;
+            }
+            if (changed) {
+                gs_free char *strtmp = NULL;
+
+                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s",
+                      key,
+                      ovs_port->name,
+                      NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
+                                           ", ",
+                                           ovs_port->connection_uuid,
+                                           ""),
+                      (strtmp = _external_ids_to_string(ovs_port->external_ids)));
+            }
+        } else {
+            gs_free char *strtmp = NULL;
+
+            ovs_port  = g_slice_new(OpenvswitchPort);
+            *ovs_port = (OpenvswitchPort){
+                .port_uuid       = g_strdup(key),
+                .name            = g_strdup(name),
+                .connection_uuid = g_strdup(connection_uuid),
+                .interfaces      = g_steal_pointer(&interfaces),
+                .external_ids    = g_steal_pointer(&external_ids_arr),
+            };
+            g_hash_table_add(priv->ports, ovs_port);
+            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s",
+                  key,
+                  ovs_port->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
+                                       ", ",
+                                       ovs_port->connection_uuid,
+                                       ""),
+                  (strtmp = _external_ids_to_string(ovs_port->external_ids)));
+            _signal_emit_device_added(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
+        }
+    }
+
+    json_object_foreach (bridge, key, value) {
+        gs_unref_ptrarray GPtrArray *ports = NULL;
+        OpenvswitchBridge *          ovs_bridge;
+        gs_unref_array GArray *external_ids_arr = NULL;
+        const char *           connection_uuid  = NULL;
+        int                    r;
+
+        r = json_unpack(value,
+                        "{s:{s:s, s:o, s:o}}",
+                        "new",
+                        "name",
+                        &name,
+                        "external_ids",
+                        &external_ids,
+                        "ports",
+                        &items);
+
+        if (r != 0) {
+            gpointer unused;
+
+            r = json_unpack(value, "{s:{}}", "old");
+            if (r != 0)
+                continue;
+
+            if (!g_hash_table_steal_extended(priv->bridges,
+                                             &key,
+                                             (gpointer *) &ovs_bridge,
+                                             &unused))
+                continue;
+
+            _LOGT("obj[bridge:%s]: removed a bridge: %s%s%s",
+                  key,
+                  ovs_bridge->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
+                                       ", ",
+                                       ovs_bridge->connection_uuid,
+                                       ""));
+            _signal_emit_device_removed(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
+            _free_bridge(ovs_bridge);
+            continue;
+        }
+
+        ovs_bridge = g_hash_table_lookup(priv->bridges, &key);
+
+        if (ovs_bridge && !nm_streq0(ovs_bridge->name, name)) {
+            if (!g_hash_table_steal(priv->bridges, ovs_bridge))
+                nm_assert_not_reached();
+            _signal_emit_device_removed(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
+            nm_clear_pointer(&ovs_bridge, _free_bridge);
+        }
+
+        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        ports = _uuids_to_array(items);
+
+        if (ovs_bridge) {
+            gboolean changed = FALSE;
+
+            nm_assert(nm_streq0(ovs_bridge->name, name));
+
+            changed = nm_utils_strdup_reset(&ovs_bridge->name, name);
+            changed = nm_utils_strdup_reset(&ovs_bridge->connection_uuid, connection_uuid);
+            if (nm_strv_ptrarray_cmp(ovs_bridge->ports, ports) != 0) {
+                NM_SWAP(&ovs_bridge->ports, &ports);
+                changed = TRUE;
+            }
+            if (!_external_ids_equal(ovs_bridge->external_ids, external_ids_arr)) {
+                NM_SWAP(&ovs_bridge->external_ids, &external_ids_arr);
+                changed = TRUE;
+            }
+            if (changed) {
+                gs_free char *strtmp = NULL;
+
+                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s",
+                      key,
+                      ovs_bridge->name,
+                      NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
+                                           ", ",
+                                           ovs_bridge->connection_uuid,
+                                           ""),
+                      (strtmp = _external_ids_to_string(ovs_bridge->external_ids)));
+            }
+        } else {
+            gs_free char *strtmp = NULL;
+
+            ovs_bridge  = g_slice_new(OpenvswitchBridge);
+            *ovs_bridge = (OpenvswitchBridge){
+                .bridge_uuid     = g_strdup(key),
+                .name            = g_strdup(name),
+                .connection_uuid = g_strdup(connection_uuid),
+                .ports           = g_steal_pointer(&ports),
+                .external_ids    = g_steal_pointer(&external_ids_arr),
+            };
+            g_hash_table_add(priv->bridges, ovs_bridge);
+            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s",
+                  key,
+                  ovs_bridge->name,
+                  NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
+                                       ", ",
+                                       ovs_bridge->connection_uuid,
+                                       ""),
+                  (strtmp = _external_ids_to_string(ovs_bridge->external_ids)));
+            _signal_emit_device_added(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
+        }
+    }
+}
+
+/**
+ * ovsdb_got_echo:
+ *
+ * Only implemented because the specification mandates it. Actual ovsdb hasn't been
+ * seen doing this.
+ */
+static void
+ovsdb_got_echo(NMOvsdb *self, json_int_t id, json_t *data)
+{
+    NMOvsdbPrivate *    priv        = NM_OVSDB_GET_PRIVATE(self);
+    nm_auto_decref_json json_t *msg = NULL;
+    char *                      reply;
+    gboolean                    output_was_empty;
+
+    output_was_empty = priv->output->len == 0;
+
+    msg   = json_pack("{s:I, s:O}", "id", id, "result", data);
+    reply = json_dumps(msg, 0);
+    g_string_append(priv->output, reply);
+    free(reply);
+
+    if (output_was_empty)
+        ovsdb_write(self);
+}
+
+/**
+ * ovsdb_got_msg::
+ *
+ * Called when a complete JSON object was seen and unmarshalled.
+ * Either finishes a method call or processes a method call.
+ */
+static void
+ovsdb_got_msg(NMOvsdb *self, json_t *msg)
+{
+    NMOvsdbPrivate *priv       = NM_OVSDB_GET_PRIVATE(self);
+    json_error_t    json_error = {
+        0,
+    };
+    json_t *    json_id = NULL;
+    json_int_t  id      = (json_int_t) -1;
+    const char *method  = NULL;
+    json_t *    params  = NULL;
+    json_t *    result  = NULL;
+    json_t *    error   = NULL;
+
+    if (json_unpack_ex(msg,
+                       &json_error,
+                       0,
+                       "{s?:o, s?:s, s?:o, s?:o, s?:o}",
+                       "id",
+                       &json_id,
+                       "method",
+                       &method,
+                       "params",
+                       &params,
+                       "result",
+                       &result,
+                       "error",
+                       &error)
+        == -1) {
+        _LOGW("couldn't grok the message: %s", json_error.text);
+        ovsdb_disconnect(self, FALSE, FALSE);
+        return;
+    }
+
+    if (json_is_number(json_id))
+        id = json_integer_value(json_id);
+
+    if (method) {
+        /* It's a method call! */
+        if (!params) {
+            _LOGW("a method call with no params: '%s'", method);
+            ovsdb_disconnect(self, FALSE, FALSE);
+            return;
+        }
+
+        if (nm_streq0(method, "update")) {
+            /* This is a update method call. */
+            ovsdb_got_update(self, json_array_get(params, 1));
+        } else if (nm_streq0(method, "echo")) {
+            /* This is an echo request. */
+            ovsdb_got_echo(self, id, params);
+        } else {
+            _LOGW("got an unknown method call: '%s'", method);
+        }
+        return;
+    }
+
+    if (id >= 0) {
+        OvsdbMethodCall *call;
+        gs_free_error GError *local      = NULL;
+        gs_free char *        msg_as_str = NULL;
+
+        /* This is a response to a method call. */
+        if (c_list_is_empty(&priv->calls_lst_head)) {
+            _LOGE("there are no queued calls expecting response %" G_GUINT64_FORMAT, (guint64) id);
+            ovsdb_disconnect(self, FALSE, FALSE);
+            return;
+        }
+        call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst);
+        if (call->call_id != id) {
+            _LOGE("expected a response to call %" G_GUINT64_FORMAT ", not %" G_GUINT64_FORMAT,
+                  call->call_id,
+                  (guint64) id);
+            ovsdb_disconnect(self, FALSE, FALSE);
+            return;
+        }
+        /* Cool, we found a corresponding call. Finish it. */
+
+        _LOGT_call(call, "response: %s", (msg_as_str = json_dumps(msg, 0)));
+
+        if (!json_is_null(error)) {
+            /* The response contains an error. */
+            g_set_error(&local,
+                        G_IO_ERROR,
+                        G_IO_ERROR_FAILED,
+                        "Error call to OVSDB returned an error: %s",
+                        json_string_value(error));
+        }
+
+        _call_complete(call, result, local);
+
+        priv->num_failures = 0;
+
+        /* Don't progress further commands in case the callback hit an error
+         * and disconnected us. */
+        if (!priv->conn)
+            return;
+
+        /* Now we're free to serialize and send the next command, if any. */
+        ovsdb_next_command(self);
+
+        return;
+    }
+
+    /* This is a message we are not interested in. */
+    _LOGW("got an unknown message, ignoring");
+}
+
+/*****************************************************************************/
+
+/* Lower level marshalling and demarshalling of the JSON-RPC traffic on the
+ * ovsdb socket. */
+
+static size_t
+_json_callback(void *buffer, size_t buflen, void *user_data)
+{
+    NMOvsdb *       self = NM_OVSDB(user_data);
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    if (priv->bufp == priv->input->len) {
+        /* No more bytes buffered for decoding. */
+        return 0;
+    }
+
+    /* Pass one more byte to the JSON decoder. */
+    *(char *) buffer = priv->input->str[priv->bufp];
+    priv->bufp++;
+
+    return (size_t) 1;
+}
+
+/**
+ * ovsdb_read_cb:
+ *
+ * Read out the data available from the ovsdb socket and try to deserialize
+ * the JSON. If we see a complete object, pass it upwards to ovsdb_got_msg().
+ */
+static void
+ovsdb_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMOvsdb *       self   = NM_OVSDB(user_data);
+    NMOvsdbPrivate *priv   = NM_OVSDB_GET_PRIVATE(self);
+    GInputStream *  stream = G_INPUT_STREAM(source_object);
+    GError *        error  = NULL;
+    gssize          size;
+    json_t *        msg;
+    json_error_t    json_error = {
+        0,
+    };
+
+    size = g_input_stream_read_finish(stream, res, &error);
+    if (size == -1) {
+        /* ovsdb-server was possibly restarted */
+        _LOGW("short read from ovsdb: %s", error->message);
+        priv->num_failures++;
+        g_clear_error(&error);
+        ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
+        return;
+    }
+
+    g_string_append_len(priv->input, priv->buf, size);
+    do {
+        priv->bufp = 0;
+        /* The callback always eats up only up to a single byte. This makes
+         * it possible for us to identify complete JSON objects in spite of
+         * us not knowing the length in advance. */
+        msg = json_load_callback(_json_callback, self, JSON_DISABLE_EOF_CHECK, &json_error);
+        if (msg) {
+            ovsdb_got_msg(self, msg);
+            g_string_erase(priv->input, 0, priv->bufp);
+        }
+        json_decref(msg);
+    } while (msg);
+
+    if (!priv->conn)
+        return;
+
+    if (size)
+        ovsdb_read(self);
+}
+
+static void
+ovsdb_read(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    g_input_stream_read_async(g_io_stream_get_input_stream(G_IO_STREAM(priv->conn)),
+                              priv->buf,
+                              sizeof(priv->buf),
+                              G_PRIORITY_DEFAULT,
+                              NULL,
+                              ovsdb_read_cb,
+                              self);
+}
+
+static void
+ovsdb_write_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GOutputStream * stream = G_OUTPUT_STREAM(source_object);
+    NMOvsdb *       self   = NM_OVSDB(user_data);
+    NMOvsdbPrivate *priv   = NM_OVSDB_GET_PRIVATE(self);
+    GError *        error  = NULL;
+    gssize          size;
+
+    size = g_output_stream_write_finish(stream, res, &error);
+    if (size == -1) {
+        /* ovsdb-server was possibly restarted */
+        _LOGW("short write to ovsdb: %s", error->message);
+        priv->num_failures++;
+        g_clear_error(&error);
+        ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
+        return;
+    }
+
+    if (!priv->conn)
+        return;
+
+    g_string_erase(priv->output, 0, size);
+
+    ovsdb_write(self);
+}
+
+static void
+ovsdb_write(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+    GOutputStream * stream;
+
+    if (!priv->output->len)
+        return;
+
+    stream = g_io_stream_get_output_stream(G_IO_STREAM(priv->conn));
+    if (g_output_stream_has_pending(stream))
+        return;
+
+    g_output_stream_write_async(stream,
+                                priv->output->str,
+                                priv->output->len,
+                                G_PRIORITY_DEFAULT,
+                                NULL,
+                                ovsdb_write_cb,
+                                self);
+}
+
+/*****************************************************************************/
+
+/* Routines to maintain the ovsdb connection. */
+
+/**
+ * ovsdb_disconnect:
+ *
+ * Clean up the internal state to the point equivalent to before connecting.
+ * Apart from clean shutdown this is a good response to unexpected trouble,
+ * since the next method call attempt a will trigger reconnect which hopefully
+ * puts us back in sync.
+ */
+static void
+ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing)
+{
+    NMOvsdbPrivate * priv = NM_OVSDB_GET_PRIVATE(self);
+    OvsdbMethodCall *call;
+
+    nm_assert(!retry || !is_disposing);
+
+    if (!priv->client)
+        return;
+
+    _LOGD("disconnecting from ovsdb, retry %d", retry);
+
+    /* FIXME(shutdown): NMOvsdb should process the pending calls before
+     * shutting down, and cancel the remaining calls after the timeout. */
+
+    if (retry) {
+        if (!c_list_is_empty(&priv->calls_lst_head)) {
+            call          = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst);
+            call->call_id = CALL_ID_UNSPEC;
+        }
+    } else {
+        gs_free_error GError *error = NULL;
+
+        if (is_disposing)
+            nm_utils_error_set_cancelled(&error, is_disposing, "NMOvsdb");
+        else
+            nm_utils_error_set(&error, NM_UTILS_ERROR_NOT_READY, "disconnected from ovsdb");
+        while ((call = c_list_last_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst)))
+            _call_complete(call, NULL, error);
+    }
+
+    priv->bufp = 0;
+    g_string_truncate(priv->input, 0);
+    g_string_truncate(priv->output, 0);
+    g_clear_object(&priv->client);
+    g_clear_object(&priv->conn);
+    nm_clear_g_free(&priv->db_uuid);
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    if (retry)
+        ovsdb_try_connect(self);
+}
+
+static void
+_check_ready(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    nm_assert(!priv->ready);
+
+    if (priv->num_pending_deletions == 0) {
+        priv->ready = TRUE;
+        g_signal_emit(self, signals[READY], 0);
+        nm_manager_unblock_failed_ovs_interfaces(nm_manager_get());
+    }
+}
+
+static void
+_del_initial_iface_cb(GError *error, gpointer user_data)
+{
+    NMOvsdb *       self;
+    gs_free char *  ifname = NULL;
+    NMOvsdbPrivate *priv;
+
+    nm_utils_user_data_unpack(user_data, &self, &ifname);
+
+    if (nm_utils_error_is_cancelled_or_disposing(error))
+        return;
+
+    priv = NM_OVSDB_GET_PRIVATE(self);
+    nm_assert(priv->num_pending_deletions > 0);
+    priv->num_pending_deletions--;
+
+    _LOGD("delete initial interface '%s': %s %s%s%s, pending %u",
+          ifname,
+          error ? "error" : "success",
+          error ? "(" : "",
+          error ? error->message : "",
+          error ? ")" : "",
+          priv->num_pending_deletions);
+
+    _check_ready(self);
+}
+
+static void
+ovsdb_cleanup_initial_interfaces(NMOvsdb *self)
+{
+    NMOvsdbPrivate *            priv = NM_OVSDB_GET_PRIVATE(self);
+    const OpenvswitchInterface *interface;
+    NMUtilsUserData *           data;
+    GHashTableIter              iter;
+
+    if (priv->ready || priv->num_pending_deletions != 0)
+        return;
+
+    /* Delete OVS interfaces added by NM. Bridges and ports and
+     * not considered because they are deleted automatically
+     * when no interface is present. */
+    g_hash_table_iter_init(&iter, self->_priv.interfaces);
+    while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &interface)) {
+        if (interface->connection_uuid) {
+            priv->num_pending_deletions++;
+            _LOGD("deleting initial interface '%s' (pending: %u)",
+                  interface->name,
+                  priv->num_pending_deletions);
+            data = nm_utils_user_data_pack(self, g_strdup(interface->name));
+            nm_ovsdb_del_interface(self, interface->name, _del_initial_iface_cb, data);
+        }
+    }
+
+    _check_ready(self);
+}
+
+static void
+_monitor_bridges_cb(NMOvsdb *self, json_t *result, GError *error, gpointer user_data)
+{
+    if (error) {
+        if (!nm_utils_error_is_cancelled_or_disposing(error)) {
+            _LOGI("%s", error->message);
+            ovsdb_disconnect(self, FALSE, FALSE);
+        }
+        return;
+    }
+
+    /* Treat the first response the same as the subsequent "update"
+     * messages we eventually get. */
+    ovsdb_got_update(self, result);
+
+    ovsdb_cleanup_initial_interfaces(self);
+}
+
+static void
+_client_connect_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GSocketClient *    client = G_SOCKET_CLIENT(source_object);
+    NMOvsdb *          self   = NM_OVSDB(user_data);
+    NMOvsdbPrivate *   priv;
+    GError *           error = NULL;
+    GSocketConnection *conn;
+
+    conn = g_socket_client_connect_finish(client, res, &error);
+    if (conn == NULL) {
+        if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+            _LOGI("%s", error->message);
+
+        ovsdb_disconnect(self, FALSE, FALSE);
+        g_clear_error(&error);
+        return;
+    }
+
+    priv       = NM_OVSDB_GET_PRIVATE(self);
+    priv->conn = conn;
+    g_clear_object(&priv->cancellable);
+
+    ovsdb_read(self);
+    ovsdb_next_command(self);
+}
+
+/**
+ * ovsdb_try_connect:
+ *
+ * Establish a connection to ovsdb unless it's already established or being
+ * established. Queues a monitor command as a very first one so that we're in
+ * sync when other commands are issued.
+ */
+static void
+ovsdb_try_connect(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+    GSocketAddress *addr;
+
+    if (priv->client)
+        return;
+
+    /* TODO: This should probably be made configurable via NetworkManager.conf */
+    addr = g_unix_socket_address_new(RUNSTATEDIR "/openvswitch/db.sock");
+
+    priv->client      = g_socket_client_new();
+    priv->cancellable = g_cancellable_new();
+    g_socket_client_connect_async(priv->client,
+                                  G_SOCKET_CONNECTABLE(addr),
+                                  priv->cancellable,
+                                  _client_connect_cb,
+                                  self);
+    g_object_unref(addr);
+
+    /* Queue a monitor call before any other command, ensuring that we have an up
+     * to date view of existing bridged that we need for add and remove ops. */
+    ovsdb_call_method(self,
+                      _monitor_bridges_cb,
+                      NULL,
+                      TRUE,
+                      OVSDB_MONITOR,
+                      OVSDB_METHOD_PAYLOAD_MONITOR());
+}
+
+/*****************************************************************************/
+
+/* Public functions useful for NMDeviceOpenvswitch to maintain the life cycle of
+ * their ovsdb entries without having to deal with ovsdb complexities themselves. */
+
+typedef struct {
+    NMOvsdbCallback callback;
+    gpointer        user_data;
+} OvsdbCall;
+
+static void
+_transact_cb(NMOvsdb *self, json_t *result, GError *error, gpointer user_data)
+{
+    OvsdbCall * call = user_data;
+    const char *err;
+    const char *err_details;
+    size_t      index;
+    json_t *    value;
+
+    if (error)
+        goto out;
+
+    json_array_foreach (result, index, value) {
+        if (json_unpack(value, "{s:s, s:s}", "error", &err, "details", &err_details) == 0) {
+            g_set_error(&error,
+                        G_IO_ERROR,
+                        G_IO_ERROR_FAILED,
+                        "Error running the transaction: %s: %s",
+                        err,
+                        err_details);
+            goto out;
+        }
+    }
+
+out:
+    call->callback(error, call->user_data);
+    nm_g_slice_free(call);
+}
+
+static OvsdbCall *
+ovsdb_call_new(NMOvsdbCallback callback, gpointer user_data)
+{
+    OvsdbCall *call;
+
+    call  = g_slice_new(OvsdbCall);
+    *call = (OvsdbCall){
+        .callback  = callback,
+        .user_data = user_data,
+    };
+    return call;
+}
+
+gboolean
+nm_ovsdb_is_ready(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    return priv->ready;
+}
+
+void
+nm_ovsdb_add_interface(NMOvsdb *       self,
+                       NMConnection *  bridge,
+                       NMConnection *  port,
+                       NMConnection *  interface,
+                       NMDevice *      bridge_device,
+                       NMDevice *      interface_device,
+                       NMOvsdbCallback callback,
+                       gpointer        user_data)
+{
+    ovsdb_call_method(self,
+                      _transact_cb,
+                      ovsdb_call_new(callback, user_data),
+                      FALSE,
+                      OVSDB_ADD_INTERFACE,
+                      OVSDB_METHOD_PAYLOAD_ADD_INTERFACE(bridge,
+                                                         port,
+                                                         interface,
+                                                         bridge_device,
+                                                         interface_device));
+}
+
+void
+nm_ovsdb_del_interface(NMOvsdb *       self,
+                       const char *    ifname,
+                       NMOvsdbCallback callback,
+                       gpointer        user_data)
+{
+    ovsdb_call_method(self,
+                      _transact_cb,
+                      ovsdb_call_new(callback, user_data),
+                      FALSE,
+                      OVSDB_DEL_INTERFACE,
+                      OVSDB_METHOD_PAYLOAD_DEL_INTERFACE(ifname));
+}
+
+void
+nm_ovsdb_set_interface_mtu(NMOvsdb *       self,
+                           const char *    ifname,
+                           guint32         mtu,
+                           NMOvsdbCallback callback,
+                           gpointer        user_data)
+{
+    ovsdb_call_method(self,
+                      _transact_cb,
+                      ovsdb_call_new(callback, user_data),
+                      FALSE,
+                      OVSDB_SET_INTERFACE_MTU,
+                      OVSDB_METHOD_PAYLOAD_SET_INTERFACE_MTU(ifname, mtu));
+}
+
+void
+nm_ovsdb_set_external_ids(NMOvsdb *                self,
+                          NMDeviceType             device_type,
+                          const char *             ifname,
+                          const char *             connection_uuid,
+                          NMSettingOvsExternalIDs *s_exid_old,
+                          NMSettingOvsExternalIDs *s_exid_new)
+{
+    gs_unref_hashtable GHashTable *exid_old = NULL;
+    gs_unref_hashtable GHashTable *exid_new = NULL;
+
+    exid_old = s_exid_old
+                   ? nm_utils_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_old))
+                   : NULL;
+    exid_new = s_exid_new
+                   ? nm_utils_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_new))
+                   : NULL;
+
+    ovsdb_call_method(self,
+                      NULL,
+                      NULL,
+                      FALSE,
+                      OVSDB_SET_EXTERNAL_IDS,
+                      OVSDB_METHOD_PAYLOAD_SET_EXTERNAL_IDS(device_type,
+                                                            ifname,
+                                                            connection_uuid,
+                                                            exid_old,
+                                                            exid_new));
+}
+
+/*****************************************************************************/
+
+static void
+nm_ovsdb_init(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    c_list_init(&priv->calls_lst_head);
+
+    priv->input  = g_string_new(NULL);
+    priv->output = g_string_new(NULL);
+    priv->bridges =
+        g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_bridge, NULL);
+    priv->ports =
+        g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_port, NULL);
+    priv->interfaces =
+        g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_interface, NULL);
+
+    ovsdb_try_connect(self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMOvsdb *       self = NM_OVSDB(object);
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    ovsdb_disconnect(self, FALSE, TRUE);
+
+    nm_assert(c_list_is_empty(&priv->calls_lst_head));
+
+    if (priv->input) {
+        g_string_free(priv->input, TRUE);
+        priv->input = NULL;
+    }
+    if (priv->output) {
+        g_string_free(priv->output, TRUE);
+        priv->output = NULL;
+    }
+
+    nm_clear_pointer(&priv->bridges, g_hash_table_destroy);
+    nm_clear_pointer(&priv->ports, g_hash_table_destroy);
+    nm_clear_pointer(&priv->interfaces, g_hash_table_destroy);
+
+    G_OBJECT_CLASS(nm_ovsdb_parent_class)->dispose(object);
+}
+
+static void
+nm_ovsdb_class_init(NMOvsdbClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    signals[DEVICE_ADDED] = g_signal_new(NM_OVSDB_DEVICE_ADDED,
+                                         G_OBJECT_CLASS_TYPE(object_class),
+                                         G_SIGNAL_RUN_LAST,
+                                         0,
+                                         NULL,
+                                         NULL,
+                                         NULL,
+                                         G_TYPE_NONE,
+                                         3,
+                                         G_TYPE_STRING,
+                                         G_TYPE_UINT,
+                                         G_TYPE_STRING);
+
+    signals[DEVICE_REMOVED] = g_signal_new(NM_OVSDB_DEVICE_REMOVED,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_LAST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           3,
+                                           G_TYPE_STRING,
+                                           G_TYPE_UINT,
+                                           G_TYPE_STRING);
+
+    signals[INTERFACE_FAILED] = g_signal_new(NM_OVSDB_INTERFACE_FAILED,
+                                             G_OBJECT_CLASS_TYPE(object_class),
+                                             G_SIGNAL_RUN_LAST,
+                                             0,
+                                             NULL,
+                                             NULL,
+                                             NULL,
+                                             G_TYPE_NONE,
+                                             3,
+                                             G_TYPE_STRING,
+                                             G_TYPE_STRING,
+                                             G_TYPE_STRING);
+
+    signals[READY] = g_signal_new(NM_OVSDB_READY,
+                                  G_OBJECT_CLASS_TYPE(object_class),
+                                  G_SIGNAL_RUN_LAST,
+                                  0,
+                                  NULL,
+                                  NULL,
+                                  NULL,
+                                  G_TYPE_NONE,
+                                  0);
+}
diff --git a/src/core/devices/ovs/nm-ovsdb.h b/src/core/devices/ovs/nm-ovsdb.h
new file mode 100644
index 00000000..d0b4d19d
--- /dev/null
+++ b/src/core/devices/ovs/nm-ovsdb.h
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_OVSDB_H__
+#define __NETWORKMANAGER_OVSDB_H__
+
+#define NM_TYPE_OVSDB            (nm_ovsdb_get_type())
+#define NM_OVSDB(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_OVSDB, NMOvsdb))
+#define NM_OVSDB_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_OVSDB, NMOvsdbClass))
+#define NM_IS_OVSDB(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_OVSDB))
+#define NM_IS_OVSDB_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_OVSDB))
+#define NM_OVSDB_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_OVSDB, NMOvsdbClass))
+
+#define NM_OVSDB_DEVICE_ADDED     "device-added"
+#define NM_OVSDB_DEVICE_REMOVED   "device-removed"
+#define NM_OVSDB_INTERFACE_FAILED "interface-failed"
+#define NM_OVSDB_READY            "ready"
+
+typedef struct _NMOvsdb      NMOvsdb;
+typedef struct _NMOvsdbClass NMOvsdbClass;
+
+typedef void (*NMOvsdbCallback)(GError *error, gpointer user_data);
+
+NMOvsdb *nm_ovsdb_get(void);
+
+GType nm_ovsdb_get_type(void);
+
+void nm_ovsdb_add_interface(NMOvsdb *       self,
+                            NMConnection *  bridge,
+                            NMConnection *  port,
+                            NMConnection *  interface,
+                            NMDevice *      bridge_device,
+                            NMDevice *      interface_device,
+                            NMOvsdbCallback callback,
+                            gpointer        user_data);
+
+void nm_ovsdb_del_interface(NMOvsdb *       self,
+                            const char *    ifname,
+                            NMOvsdbCallback callback,
+                            gpointer        user_data);
+
+void nm_ovsdb_set_interface_mtu(NMOvsdb *       self,
+                                const char *    ifname,
+                                guint32         mtu,
+                                NMOvsdbCallback callback,
+                                gpointer        user_data);
+
+struct _NMSettingOvsExternalIDs;
+
+void nm_ovsdb_set_external_ids(NMOvsdb *                        self,
+                               NMDeviceType                     device_type,
+                               const char *                     ifname,
+                               const char *                     connection_uuid,
+                               struct _NMSettingOvsExternalIDs *s_exid_old,
+                               struct _NMSettingOvsExternalIDs *s_exid_new);
+
+gboolean nm_ovsdb_is_ready(NMOvsdb *self);
+
+#endif /* __NETWORKMANAGER_OVSDB_H__ */
diff --git a/src/core/devices/team/meson.build b/src/core/devices/team/meson.build
new file mode 100644
index 00000000..d0ff4caa
--- /dev/null
+++ b/src/core/devices/team/meson.build
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_device_plugin_team = shared_module(
+  'nm-device-plugin-team',
+  sources: files(
+    'nm-device-team.c',
+    'nm-team-factory.c',
+  ),
+  dependencies: [
+    core_plugin_dep,
+    jansson_dep,
+    libteamdctl_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_team
+
+test(
+  'check-local-devices-team',
+  check_exports,
+  args: [
+    libnm_device_plugin_team.full_path(),
+    linker_script_devices,
+  ],
+)
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
new file mode 100644
index 00000000..d2d71729
--- /dev/null
+++ b/src/core/devices/team/nm-device-team.c
@@ -0,0 +1,1096 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Jiri Pirko <jiri@resnulli.us>
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-team.h"
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <teamdctl.h>
+#include <stdlib.h>
+
+#include "nm-glib-aux/nm-jansson.h"
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-config.h"
+#include "nm-core-internal.h"
+#include "nm-dbus-manager.h"
+#include "nm-ip4-config.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceTeam
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceTeam, PROP_CONFIG, );
+
+typedef struct {
+    struct teamdctl *  tdc;
+    char *             config;
+    GPid               teamd_pid;
+    guint              teamd_process_watch;
+    guint              teamd_timeout;
+    guint              teamd_read_timeout;
+    guint              teamd_dbus_watch;
+    bool               kill_in_progress : 1;
+    GFileMonitor *     usock_monitor;
+    NMDeviceStageState stage1_state : 3;
+} NMDeviceTeamPrivate;
+
+struct _NMDeviceTeam {
+    NMDevice            parent;
+    NMDeviceTeamPrivate _priv;
+};
+
+struct _NMDeviceTeamClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceTeam, nm_device_team, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_TEAM_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceTeam, NM_IS_DEVICE_TEAM, NMDevice)
+
+/*****************************************************************************/
+
+static gboolean teamd_start(NMDeviceTeam *self);
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingTeam *s_team;
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_TEAM_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Team connection"),
+                              "team",
+                              NULL,
+                              TRUE);
+
+    s_team = nm_connection_get_setting_team(connection);
+    if (!s_team) {
+        s_team = (NMSettingTeam *) nm_setting_team_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_team));
+    }
+
+    return TRUE;
+}
+
+static gboolean
+ensure_teamd_connection(NMDevice *device)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    int                  err;
+
+    if (priv->tdc)
+        return TRUE;
+
+    priv->tdc = teamdctl_alloc();
+    g_assert(priv->tdc);
+    err = teamdctl_connect(priv->tdc, nm_device_get_iface(device), NULL, NULL);
+    if (err != 0) {
+        _LOGE(LOGD_TEAM, "failed to connect to teamd (err=%d)", err);
+        teamdctl_free(priv->tdc);
+        priv->tdc = NULL;
+    }
+
+    return !!priv->tdc;
+}
+
+static const char *
+_get_config(NMDeviceTeam *self)
+{
+    return nm_str_not_empty(NM_DEVICE_TEAM_GET_PRIVATE(self)->config);
+}
+
+static gboolean
+teamd_read_config(NMDeviceTeam *self)
+{
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    const char *         config = NULL;
+    int                  err;
+
+    if (priv->tdc) {
+        err = teamdctl_config_actual_get_raw_direct(priv->tdc, (char **) &config);
+        if (err)
+            return FALSE;
+        if (!config) {
+            /* set "" to distinguish an empty result from no config at all. */
+            config = "";
+        }
+    }
+
+    if (!nm_streq0(config, priv->config)) {
+        g_free(priv->config);
+        priv->config = g_strdup(config);
+        _notify(self, PROP_CONFIG);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+teamd_read_timeout_cb(gpointer user_data)
+{
+    NMDeviceTeam *       self = user_data;
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    priv->teamd_read_timeout = 0;
+    teamd_read_config(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    NMDeviceTeam *       self   = NM_DEVICE_TEAM(device);
+    NMSettingTeam *      s_team = nm_connection_get_setting_team(connection);
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    struct teamdctl *    tdc    = priv->tdc;
+
+    if (!s_team) {
+        s_team = (NMSettingTeam *) nm_setting_team_new();
+        nm_connection_add_setting(connection, (NMSetting *) s_team);
+    }
+
+    /* Read the configuration only if not already set */
+    if (!priv->config && ensure_teamd_connection(device))
+        teamd_read_config(self);
+
+    /* Restore previous tdc state */
+    if (priv->tdc && !tdc) {
+        teamdctl_disconnect(priv->tdc);
+        teamdctl_free(priv->tdc);
+        priv->tdc = NULL;
+    }
+
+    g_object_set(G_OBJECT(s_team), NM_SETTING_TEAM_CONFIG, _get_config(self), NULL);
+}
+
+/*****************************************************************************/
+
+static gboolean
+master_update_slave_connection(NMDevice *    self,
+                               NMDevice *    slave,
+                               NMConnection *connection,
+                               GError **     error)
+{
+    NMSettingTeamPort *s_port;
+    char *             port_config = NULL;
+    int                err         = 0;
+    struct teamdctl *  tdc;
+    const char *       team_port_config = NULL;
+    const char *       iface            = nm_device_get_iface(self);
+    const char *       iface_slave      = nm_device_get_iface(slave);
+
+    tdc = teamdctl_alloc();
+    if (!tdc) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "update slave connection for slave '%s' failed to connect to teamd for master "
+                    "%s (out of memory?)",
+                    iface_slave,
+                    iface);
+        g_return_val_if_reached(FALSE);
+    }
+
+    err = teamdctl_connect(tdc, iface, NULL, NULL);
+    if (err) {
+        teamdctl_free(tdc);
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "update slave connection for slave '%s' failed to connect to teamd for master "
+                    "%s (err=%d)",
+                    iface_slave,
+                    iface,
+                    err);
+        return FALSE;
+    }
+
+    err = teamdctl_port_config_get_raw_direct(tdc, iface_slave, (char **) &team_port_config);
+    port_config = g_strdup(team_port_config);
+    teamdctl_disconnect(tdc);
+    teamdctl_free(tdc);
+    if (err) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_FAILED,
+                    "update slave connection for slave '%s' failed to get configuration from teamd "
+                    "master %s (err=%d)",
+                    iface_slave,
+                    iface,
+                    err);
+        g_free(port_config);
+        return FALSE;
+    }
+
+    s_port = nm_connection_get_setting_team_port(connection);
+    if (!s_port) {
+        s_port = (NMSettingTeamPort *) nm_setting_team_port_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_port));
+    }
+
+    g_object_set(G_OBJECT(s_port), NM_SETTING_TEAM_PORT_CONFIG, port_config, NULL);
+    g_free(port_config);
+
+    g_object_set(nm_connection_get_setting_connection(connection),
+                 NM_SETTING_CONNECTION_MASTER,
+                 iface,
+                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_TEAM_SETTING_NAME,
+                 NULL);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+teamd_kill_cb(pid_t pid, gboolean success, int child_status, void *user_data)
+{
+    gs_unref_object NMDeviceTeam *self = user_data;
+    NMDeviceTeamPrivate *         priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    priv->kill_in_progress = FALSE;
+
+    if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_PREPARE) {
+        _LOGT(LOGD_TEAM, "kill terminated");
+        return;
+    }
+
+    _LOGT(LOGD_TEAM, "kill terminated, starting teamd...");
+    if (!teamd_start(self)) {
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+    }
+}
+
+static void
+teamd_cleanup(NMDeviceTeam *self, gboolean free_tdc)
+{
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->teamd_process_watch);
+    nm_clear_g_source(&priv->teamd_timeout);
+    nm_clear_g_source(&priv->teamd_read_timeout);
+
+    if (priv->teamd_pid > 0) {
+        priv->kill_in_progress = TRUE;
+        nm_utils_kill_child_async(priv->teamd_pid,
+                                  SIGTERM,
+                                  LOGD_TEAM,
+                                  "teamd",
+                                  2000,
+                                  teamd_kill_cb,
+                                  g_object_ref(self));
+        priv->teamd_pid = 0;
+    }
+
+    if (priv->tdc && free_tdc) {
+        teamdctl_disconnect(priv->tdc);
+        teamdctl_free(priv->tdc);
+        priv->tdc = NULL;
+    }
+}
+
+static gboolean
+teamd_timeout_cb(gpointer user_data)
+{
+    NMDeviceTeam *       self   = NM_DEVICE_TEAM(user_data);
+    NMDevice *           device = NM_DEVICE(self);
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->teamd_timeout, FALSE);
+    priv->teamd_timeout = 0;
+
+    if (priv->teamd_pid && !priv->tdc) {
+        /* Timed out launching our own teamd process */
+        _LOGW(LOGD_TEAM, "teamd timed out");
+        teamd_cleanup(self, TRUE);
+
+        g_warn_if_fail(nm_device_is_activating(device));
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+    } else {
+        /* Read again the configuration after the timeout since it might
+         * have changed.
+         */
+        if (!teamd_read_config(self)) {
+            _LOGW(LOGD_TEAM, "failed to read teamd configuration");
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+        }
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+teamd_ready(NMDeviceTeam *self)
+{
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+    gboolean             success;
+
+    if (priv->kill_in_progress) {
+        /* If we are currently killing teamd, we are not
+         * interested in knowing when it becomes ready. */
+        return;
+    }
+
+    nm_device_queue_recheck_assume(device);
+
+    /* Grab a teamd control handle even if we aren't going to use it
+     * immediately.  But if we are, and grabbing it failed, fail the
+     * device activation.
+     */
+    success = ensure_teamd_connection(device);
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE
+        || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING)
+        return;
+
+    if (success)
+        success = teamd_read_config(self);
+
+    if (!success) {
+        teamd_cleanup(self, TRUE);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+        return;
+    }
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+teamd_gone(NMDeviceTeam *self)
+{
+    NMDevice *    device = NM_DEVICE(self);
+    NMDeviceState state;
+
+    teamd_cleanup(self, TRUE);
+    state = nm_device_get_state(device);
+
+    /* Attempt to respawn teamd */
+    if (state >= NM_DEVICE_STATE_PREPARE && state <= NM_DEVICE_STATE_ACTIVATED) {
+        if (!teamd_start(self)) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+        }
+    }
+}
+
+static void
+teamd_dbus_appeared(GDBusConnection *connection,
+                    const char *     name,
+                    const char *     name_owner,
+                    gpointer         user_data)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(user_data);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->teamd_dbus_watch);
+
+    _LOGI(LOGD_TEAM, "teamd appeared on D-Bus");
+
+    /* If another teamd grabbed the bus name while our teamd was starting,
+     * just ignore the death of our teamd and run with the existing one.
+     */
+    if (priv->teamd_process_watch) {
+        gs_unref_variant GVariant *ret = NULL;
+        guint32                    pid;
+
+        ret = g_dbus_connection_call_sync(connection,
+                                          DBUS_SERVICE_DBUS,
+                                          DBUS_PATH_DBUS,
+                                          DBUS_INTERFACE_DBUS,
+                                          "GetConnectionUnixProcessID",
+                                          g_variant_new("(s)", name_owner),
+                                          NULL,
+                                          G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                          2000,
+                                          NULL,
+                                          NULL);
+
+        if (ret) {
+            g_variant_get(ret, "(u)", &pid);
+            if (pid != priv->teamd_pid)
+                teamd_cleanup(self, FALSE);
+        } else {
+            /* The process that registered on the bus died. If it's
+             * the teamd instance we just started, ignore the event
+             * as we already detect the failure through the process
+             * watch. If it's a previous instance that got killed,
+             * also ignore that as our new instance will register
+             * again. */
+            _LOGD(LOGD_TEAM, "failed to determine D-Bus name owner, ignoring");
+            return;
+        }
+    }
+
+    teamd_ready(self);
+}
+
+static void
+teamd_dbus_vanished(GDBusConnection *dbus_connection, const char *name, gpointer user_data)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(user_data);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->teamd_dbus_watch);
+
+    if (!priv->tdc) {
+        /* g_bus_watch_name will always raise an initial signal, to indicate whether the
+         * name exists/not exists initially. Do not take this as a failure if it hadn't
+         * previously appeared.
+         */
+        _LOGD(LOGD_TEAM, "teamd not on D-Bus (ignored)");
+        return;
+    }
+
+    _LOGI(LOGD_TEAM, "teamd vanished from D-Bus");
+
+    teamd_gone(self);
+}
+
+static void
+monitor_changed_cb(GFileMonitor *    monitor,
+                   GFile *           file,
+                   GFile *           other_file,
+                   GFileMonitorEvent event_type,
+                   gpointer          user_data)
+{
+    NMDeviceTeam *self = NM_DEVICE_TEAM(user_data);
+
+    switch (event_type) {
+    case G_FILE_MONITOR_EVENT_CREATED:
+        _LOGI(LOGD_TEAM, "file %s was created", g_file_get_path(file));
+        teamd_ready(self);
+        break;
+    case G_FILE_MONITOR_EVENT_DELETED:
+        _LOGI(LOGD_TEAM, "file %s was deleted", g_file_get_path(file));
+        teamd_gone(self);
+        break;
+    default:;
+    }
+}
+
+static void
+teamd_process_watch_cb(GPid pid, int status, gpointer user_data)
+{
+    NMDeviceTeam *       self   = NM_DEVICE_TEAM(user_data);
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+    NMDeviceState        state  = nm_device_get_state(device);
+
+    g_return_if_fail(priv->teamd_process_watch);
+
+    _LOGD(LOGD_TEAM, "teamd %lld died with status %d", (long long) pid, status);
+    priv->teamd_pid           = 0;
+    priv->teamd_process_watch = 0;
+
+    /* If teamd quit within 5 seconds of starting, it's probably hosed
+     * and will just die again, so fail the activation.
+     */
+    if (priv->teamd_timeout && (state >= NM_DEVICE_STATE_PREPARE)
+        && (state <= NM_DEVICE_STATE_ACTIVATED)) {
+        _LOGW(LOGD_TEAM,
+              "teamd process %lld quit unexpectedly; failing activation",
+              (long long) pid);
+        teamd_cleanup(self, TRUE);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+    }
+}
+
+static void
+teamd_child_setup(gpointer user_data)
+{
+    nm_utils_setpgid(NULL);
+    signal(SIGPIPE, SIG_IGN);
+}
+
+static const char **
+teamd_env(void)
+{
+    const char **env = g_new0(const char *, 2);
+
+    if (nm_config_get_is_debug(nm_config_get()))
+        env[0] = "TEAM_LOG_OUTPUT=stderr";
+    else
+        env[0] = "TEAM_LOG_OUTPUT=syslog";
+
+    return env;
+}
+
+static gboolean
+teamd_kill(NMDeviceTeam *self, const char *teamd_binary, GError **error)
+{
+    gs_unref_ptrarray GPtrArray *argv    = NULL;
+    gs_free char *               tmp_str = NULL;
+    gs_free const char **        envp    = NULL;
+
+    if (!teamd_binary) {
+        teamd_binary = nm_utils_find_helper("teamd", NULL, error);
+        if (!teamd_binary) {
+            _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: teamd binary not found");
+            return FALSE;
+        }
+    }
+
+    argv = g_ptr_array_new();
+    g_ptr_array_add(argv, (gpointer) teamd_binary);
+    g_ptr_array_add(argv, (gpointer) "-k");
+    g_ptr_array_add(argv, (gpointer) "-t");
+    g_ptr_array_add(argv, (gpointer) nm_device_get_iface(NM_DEVICE(self)));
+    g_ptr_array_add(argv, NULL);
+
+    envp = teamd_env();
+
+    _LOGD(LOGD_TEAM, "running: %s", (tmp_str = g_strjoinv(" ", (char **) argv->pdata)));
+    return g_spawn_sync("/",
+                        (char **) argv->pdata,
+                        (char **) envp,
+                        0,
+                        teamd_child_setup,
+                        NULL,
+                        NULL,
+                        NULL,
+                        NULL,
+                        error);
+}
+
+static gboolean
+teamd_start(NMDeviceTeam *self)
+{
+    NMDeviceTeamPrivate *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    const char *         iface = nm_device_get_ip_iface(NM_DEVICE(self));
+    NMConnection *       connection;
+    gs_unref_ptrarray GPtrArray *argv = NULL;
+    gs_free_error GError *   error    = NULL;
+    gs_free char *           tmp_str  = NULL;
+    const char *             teamd_binary;
+    const char *             config;
+    nm_auto_free const char *config_free = NULL;
+    NMSettingTeam *          s_team;
+    gs_free char *           cloned_mac = NULL;
+    gs_free const char **    envp       = NULL;
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    s_team = nm_connection_get_setting_team(connection);
+    if (!s_team)
+        g_return_val_if_reached(FALSE);
+
+    nm_assert(iface);
+
+    teamd_binary = nm_utils_find_helper("teamd", NULL, NULL);
+    if (!teamd_binary) {
+        _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: teamd binary not found");
+        return FALSE;
+    }
+
+    if (priv->teamd_process_watch || priv->teamd_pid > 0 || priv->tdc) {
+        g_warn_if_reached();
+        if (!priv->teamd_pid)
+            teamd_kill(self, teamd_binary, NULL);
+        teamd_cleanup(self, TRUE);
+    }
+
+    /* Start teamd now */
+    argv = g_ptr_array_new();
+    g_ptr_array_add(argv, (gpointer) teamd_binary);
+    g_ptr_array_add(argv, (gpointer) "-o");
+    g_ptr_array_add(argv, (gpointer) "-n");
+    g_ptr_array_add(argv, (gpointer) "-U");
+    if (priv->teamd_dbus_watch)
+        g_ptr_array_add(argv, (gpointer) "-D");
+    g_ptr_array_add(argv, (gpointer) "-N");
+    g_ptr_array_add(argv, (gpointer) "-t");
+    g_ptr_array_add(argv, (gpointer) iface);
+
+    config = nm_setting_team_get_config(s_team);
+    if (!nm_device_hw_addr_get_cloned(NM_DEVICE(self),
+                                      connection,
+                                      FALSE,
+                                      &cloned_mac,
+                                      NULL,
+                                      &error)) {
+        _LOGW(LOGD_DEVICE, "set-hw-addr: %s", error->message);
+        return FALSE;
+    }
+
+    if (cloned_mac) {
+        json_t *     json, *hwaddr;
+        json_error_t jerror;
+
+        /* Inject the hwaddr property into the JSON configuration.
+         * While doing so, detect potential conflicts */
+
+        json = json_loads(config ?: "{}", JSON_REJECT_DUPLICATES, &jerror);
+        g_return_val_if_fail(json, FALSE);
+
+        hwaddr = json_object_get(json, "hwaddr");
+        if (hwaddr) {
+            if (!json_is_string(hwaddr) || !nm_streq0(json_string_value(hwaddr), cloned_mac))
+                _LOGW(LOGD_TEAM,
+                      "set-hw-addr: can't set team cloned-mac-address as the JSON configuration "
+                      "already contains \"hwaddr\"");
+        } else {
+            hwaddr = json_string(cloned_mac);
+            json_object_set(json, "hwaddr", hwaddr);
+            config = config_free =
+                json_dumps(json, JSON_INDENT(0) | JSON_ENSURE_ASCII | JSON_SORT_KEYS);
+            _LOGD(LOGD_TEAM,
+                  "set-hw-addr: injected \"hwaddr\" \"%s\" into team configuration",
+                  cloned_mac);
+            json_decref(hwaddr);
+        }
+        json_decref(json);
+    }
+
+    if (config) {
+        g_ptr_array_add(argv, (gpointer) "-c");
+        g_ptr_array_add(argv, (gpointer) config);
+    }
+
+    if (nm_logging_enabled(LOGL_DEBUG, LOGD_TEAM))
+        g_ptr_array_add(argv, (gpointer) "-gg");
+    g_ptr_array_add(argv, NULL);
+
+    envp = teamd_env();
+
+    _LOGD(LOGD_TEAM, "running: %s", (tmp_str = g_strjoinv(" ", (char **) argv->pdata)));
+    if (!g_spawn_async("/",
+                       (char **) argv->pdata,
+                       (char **) envp,
+                       G_SPAWN_DO_NOT_REAP_CHILD,
+                       teamd_child_setup,
+                       NULL,
+                       &priv->teamd_pid,
+                       &error)) {
+        _LOGW(LOGD_TEAM, "Activation: (team) failed to start teamd: %s", error->message);
+        teamd_cleanup(self, TRUE);
+        return FALSE;
+    }
+
+    /* Start a timeout for teamd to appear at D-Bus */
+    if (!priv->teamd_timeout)
+        priv->teamd_timeout = g_timeout_add_seconds(5, teamd_timeout_cb, self);
+
+    /* Monitor the child process so we know when it dies */
+    priv->teamd_process_watch = g_child_watch_add(priv->teamd_pid, teamd_process_watch_cb, self);
+
+    _LOGI(LOGD_TEAM, "Activation: (team) started teamd [pid %u]...", (guint) priv->teamd_pid);
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceTeam *       self   = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    gs_free_error GError *error = NULL;
+    NMSettingTeam *       s_team;
+    const char *          cfg;
+
+    if (nm_device_sys_iface_state_is_external(device))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    if (nm_device_sys_iface_state_is_external_or_assume(device)) {
+        if (ensure_teamd_connection(device))
+            return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    s_team = nm_device_get_applied_setting(device, NM_TYPE_SETTING_TEAM);
+    if (!s_team)
+        g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_PENDING)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_COMPLETED)
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_PENDING;
+
+    if (priv->tdc) {
+        /* If the existing teamd config is the same as we're about to use,
+         * then we can proceed.  If it's not the same, and we have a PID,
+         * kill it so we can respawn it with the right config.  If we don't
+         * have a PID, then we must fail.
+         */
+        cfg = teamdctl_config_get_raw(priv->tdc);
+        if (cfg && nm_streq0(cfg, nm_setting_team_get_config(s_team))) {
+            _LOGD(LOGD_TEAM, "using existing matching teamd config");
+            return NM_ACT_STAGE_RETURN_SUCCESS;
+        }
+
+        if (!priv->teamd_pid) {
+            _LOGD(LOGD_TEAM, "existing teamd config mismatch; killing existing via teamdctl");
+            if (!teamd_kill(self, NULL, &error)) {
+                _LOGW(LOGD_TEAM,
+                      "existing teamd config mismatch; failed to kill existing teamd: %s",
+                      error->message);
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
+                return NM_ACT_STAGE_RETURN_FAILURE;
+            }
+        }
+
+        _LOGD(LOGD_TEAM, "existing teamd config mismatch; respawning...");
+        teamd_cleanup(self, TRUE);
+    }
+
+    if (priv->kill_in_progress) {
+        _LOGT(LOGD_TEAM, "kill in progress, wait before starting teamd");
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (!teamd_start(self))
+        return NM_ACT_STAGE_RETURN_FAILURE;
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+
+    if (nm_device_sys_iface_state_is_external(device))
+        return;
+
+    if (priv->teamd_pid || priv->tdc)
+        _LOGI(LOGD_TEAM, "deactivation: stopping teamd...");
+
+    if (!priv->teamd_pid)
+        teamd_kill(self, NULL, NULL);
+
+    teamd_cleanup(self, TRUE);
+}
+
+static gboolean
+enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gboolean configure)
+{
+    NMDeviceTeam *       self        = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv        = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    gboolean             success     = TRUE;
+    const char *         slave_iface = nm_device_get_ip_iface(slave);
+    NMSettingTeamPort *  s_team_port;
+
+    nm_device_master_check_slave_physical_port(device, slave, LOGD_TEAM);
+
+    if (configure) {
+        nm_device_take_down(slave, TRUE);
+
+        s_team_port = nm_connection_get_setting_team_port(connection);
+        if (s_team_port) {
+            const char *config = nm_setting_team_port_get_config(s_team_port);
+
+            if (config) {
+                if (!priv->tdc) {
+                    _LOGW(LOGD_TEAM,
+                          "enslaved team port %s config not changed, not connected to teamd",
+                          slave_iface);
+                } else {
+                    int   err;
+                    char *sanitized_config;
+
+                    sanitized_config = g_strdelimit(g_strdup(config), "\r\n", ' ');
+                    err = teamdctl_port_config_update_raw(priv->tdc, slave_iface, sanitized_config);
+                    g_free(sanitized_config);
+                    if (err != 0) {
+                        _LOGE(LOGD_TEAM,
+                              "failed to update config for port %s (err=%d)",
+                              slave_iface,
+                              err);
+                        return FALSE;
+                    }
+                }
+            }
+        }
+        success = nm_platform_link_enslave(nm_device_get_platform(device),
+                                           nm_device_get_ip_ifindex(device),
+                                           nm_device_get_ip_ifindex(slave));
+        nm_device_bring_up(slave, TRUE, NULL);
+
+        if (!success)
+            return FALSE;
+
+        nm_clear_g_source(&priv->teamd_read_timeout);
+        priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self);
+
+        _LOGI(LOGD_TEAM, "enslaved team port %s", slave_iface);
+    } else
+        _LOGI(LOGD_TEAM, "team port %s was enslaved", slave_iface);
+
+    return TRUE;
+}
+
+static void
+release_slave(NMDevice *device, NMDevice *slave, gboolean configure)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    gboolean             do_release, success;
+    NMSettingTeamPort *  s_port;
+    int                  ifindex_slave;
+    int                  ifindex;
+
+    do_release = configure;
+    if (do_release) {
+        ifindex = nm_device_get_ifindex(device);
+        if (ifindex <= 0 || !nm_platform_link_get(nm_device_get_platform(device), ifindex))
+            do_release = FALSE;
+    }
+
+    ifindex_slave = nm_device_get_ip_ifindex(slave);
+
+    if (ifindex_slave <= 0) {
+        _LOGD(LOGD_TEAM, "team port %s is already released", nm_device_get_ip_iface(slave));
+    } else if (do_release) {
+        success = nm_platform_link_release(nm_device_get_platform(device),
+                                           nm_device_get_ip_ifindex(device),
+                                           ifindex_slave);
+        if (success)
+            _LOGI(LOGD_TEAM, "released team port %s", nm_device_get_ip_iface(slave));
+        else
+            _LOGW(LOGD_TEAM, "failed to release team port %s", nm_device_get_ip_iface(slave));
+
+        /* Kernel team code "closes" the port when releasing it, (which clears
+         * IFF_UP), so we must bring it back up here to ensure carrier changes and
+         * other state is noticed by the now-released port.
+         */
+        if (!nm_device_bring_up(slave, TRUE, NULL)) {
+            _LOGW(LOGD_TEAM,
+                  "released team port %s could not be brought up",
+                  nm_device_get_ip_iface(slave));
+        }
+
+        nm_clear_g_source(&priv->teamd_read_timeout);
+        priv->teamd_read_timeout = g_timeout_add_seconds(5, teamd_read_timeout_cb, self);
+    } else
+        _LOGI(LOGD_TEAM, "team port %s was released", nm_device_get_ip_iface(slave));
+
+    /* Delete any port configuration we previously set */
+    if (configure && priv->tdc
+        && (s_port = nm_device_get_applied_setting(slave, NM_TYPE_SETTING_TEAM_PORT))
+        && (nm_setting_team_port_get_config(s_port)))
+        teamdctl_port_config_update_raw(priv->tdc, nm_device_get_ip_iface(slave), "{}");
+}
+
+static gboolean
+create_and_realize(NMDevice *             device,
+                   NMConnection *         connection,
+                   NMDevice *             parent,
+                   const NMPlatformLink **out_plink,
+                   GError **              error)
+{
+    const char *iface = nm_device_get_iface(device);
+    int         r;
+
+    r = nm_platform_link_team_add(nm_device_get_platform(device), iface, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create team master interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceTeam *self = NM_DEVICE_TEAM(object);
+
+    switch (prop_id) {
+    case PROP_CONFIG:
+        g_value_set_string(value, _get_config(self));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_team_init(NMDeviceTeam *self)
+{
+    nm_assert(nm_device_is_master(NM_DEVICE(self)));
+}
+
+static void
+constructed(GObject *object)
+{
+    NMDevice *           device  = NM_DEVICE(object);
+    NMDeviceTeamPrivate *priv    = NM_DEVICE_TEAM_GET_PRIVATE(device);
+    gs_free char *       tmp_str = NULL;
+    gs_unref_object GFile *file  = NULL;
+    GError *               error;
+
+    G_OBJECT_CLASS(nm_device_team_parent_class)->constructed(object);
+
+    if (nm_dbus_manager_get_dbus_connection(nm_dbus_manager_get())) {
+        /* Register D-Bus name watcher */
+        tmp_str = g_strdup_printf("org.libteam.teamd.%s", nm_device_get_ip_iface(device));
+        priv->teamd_dbus_watch = g_bus_watch_name(G_BUS_TYPE_SYSTEM,
+                                                  tmp_str,
+                                                  G_BUS_NAME_WATCHER_FLAGS_NONE,
+                                                  teamd_dbus_appeared,
+                                                  teamd_dbus_vanished,
+                                                  NM_DEVICE(device),
+                                                  NULL);
+        return;
+    }
+
+    /* No D-Bus, watch unix socket */
+    tmp_str             = g_strdup_printf("/run/teamd/%s.sock", nm_device_get_ip_iface(device));
+    file                = g_file_new_for_path(tmp_str);
+    priv->usock_monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, NULL, &error);
+    if (!priv->usock_monitor) {
+        nm_log_warn(LOGD_TEAM, "error monitoring %s: %s", tmp_str, error->message);
+    } else {
+        g_signal_connect(priv->usock_monitor, "changed", G_CALLBACK(monitor_changed_cb), object);
+    }
+}
+
+NMDevice *
+nm_device_team_new(const char *iface)
+{
+    return g_object_new(NM_TYPE_DEVICE_TEAM,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_DRIVER,
+                        "team",
+                        NM_DEVICE_TYPE_DESC,
+                        "Team",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_TEAM,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_TEAM,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceTeam *       self = NM_DEVICE_TEAM(object);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+
+    if (priv->teamd_dbus_watch) {
+        g_bus_unwatch_name(priv->teamd_dbus_watch);
+        priv->teamd_dbus_watch = 0;
+    }
+
+    if (priv->usock_monitor) {
+        g_signal_handlers_disconnect_by_data(priv->usock_monitor, object);
+        g_clear_object(&priv->usock_monitor);
+    }
+
+    teamd_cleanup(self, TRUE);
+    nm_clear_g_free(&priv->config);
+
+    G_OBJECT_CLASS(nm_device_team_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_team = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_TEAM,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Carrier", "b", NM_DEVICE_CARRIER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Slaves", "ao", NM_DEVICE_SLAVES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Config",
+                                                             "s",
+                                                             NM_DEVICE_TEAM_CONFIG), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_team_class_init(NMDeviceTeamClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_team);
+
+    device_class->connection_type_supported        = NM_SETTING_TEAM_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_TEAM_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_TEAM);
+
+    device_class->is_master                      = TRUE;
+    device_class->create_and_realize             = create_and_realize;
+    device_class->get_generic_capabilities       = get_generic_capabilities;
+    device_class->complete_connection            = complete_connection;
+    device_class->update_connection              = update_connection;
+    device_class->master_update_slave_connection = master_update_slave_connection;
+
+    device_class->act_stage1_prepare_also_for_external_or_assume = TRUE;
+    device_class->act_stage1_prepare                             = act_stage1_prepare;
+    device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
+    device_class->deactivate         = deactivate;
+    device_class->enslave_slave      = enslave_slave;
+    device_class->release_slave      = release_slave;
+
+    obj_properties[PROP_CONFIG] = g_param_spec_string(NM_DEVICE_TEAM_CONFIG,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/team/nm-device-team.h b/src/core/devices/team/nm-device-team.h
new file mode 100644
index 00000000..6f5cff93
--- /dev/null
+++ b/src/core/devices/team/nm-device-team.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 Jiri Pirko <jiri@resnulli.us>
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_TEAM_H__
+#define __NETWORKMANAGER_DEVICE_TEAM_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_TEAM (nm_device_team_get_type())
+#define NM_DEVICE_TEAM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeam))
+#define NM_DEVICE_TEAM_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_TEAM, NMDeviceTeamClass))
+#define NM_IS_DEVICE_TEAM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_TEAM))
+#define NM_IS_DEVICE_TEAM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_TEAM))
+#define NM_DEVICE_TEAM_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_TEAM, NMDeviceTeamClass))
+
+/* Properties */
+#define NM_DEVICE_TEAM_CONFIG "config"
+
+typedef struct _NMDeviceTeam      NMDeviceTeam;
+typedef struct _NMDeviceTeamClass NMDeviceTeamClass;
+
+GType nm_device_team_get_type(void);
+
+NMDevice *nm_device_team_new(const char *iface);
+
+#endif /* __NETWORKMANAGER_DEVICE_TEAM_H__ */
diff --git a/src/core/devices/team/nm-team-factory.c b/src/core/devices/team/nm-team-factory.c
new file mode 100644
index 00000000..57b51bf8
--- /dev/null
+++ b/src/core/devices/team/nm-team-factory.c
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <gmodule.h>
+
+#include "nm-manager.h"
+#include "devices/nm-device-factory.h"
+#include "nm-device-team.h"
+#include "platform/nm-platform.h"
+#include "nm-core-internal.h"
+
+/*****************************************************************************/
+
+#define NM_TYPE_TEAM_FACTORY (nm_team_factory_get_type())
+#define NM_TEAM_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactory))
+#define NM_TEAM_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_TEAM_FACTORY, NMTeamFactoryClass))
+#define NM_IS_TEAM_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_TEAM_FACTORY))
+#define NM_IS_TEAM_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_TEAM_FACTORY))
+#define NM_TEAM_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_TEAM_FACTORY, NMTeamFactoryClass))
+
+typedef struct {
+    NMDeviceFactory parent;
+} NMTeamFactory;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMTeamFactoryClass;
+
+static GType nm_team_factory_get_type(void);
+
+G_DEFINE_TYPE(NMTeamFactory, nm_team_factory, NM_TYPE_DEVICE_FACTORY)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(
+    NM_LINK_TYPE_TEAM) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_TEAM_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    nm_manager_set_capability(NM_MANAGER_GET, NM_CAPABILITY_TEAM);
+    return g_object_new(NM_TYPE_TEAM_FACTORY, NULL);
+}
+
+/*****************************************************************************/
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    return nm_device_team_new(iface);
+}
+
+/*****************************************************************************/
+
+static void
+nm_team_factory_init(NMTeamFactory *self)
+{}
+
+static void
+nm_team_factory_class_init(NMTeamFactoryClass *klass)
+{
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    factory_class->create_device       = create_device;
+    factory_class->get_supported_types = get_supported_types;
+}
diff --git a/src/core/devices/tests/meson.build b/src/core/devices/tests/meson.build
new file mode 100644
index 00000000..1bc88370
--- /dev/null
+++ b/src/core/devices/tests/meson.build
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+test_units = [
+  'test-acd',
+  'test-lldp',
+]
+
+foreach test_unit: test_units
+  exe = executable(
+    test_unit,
+    test_unit + '.c',
+    dependencies: libNetworkManagerTest_dep,
+    c_args: test_c_flags,
+  )
+
+  test(
+    'devices/' + test_unit,
+    test_script,
+    args: test_args + [exe.full_path()],
+    timeout: default_test_timeout,
+  )
+endforeach
diff --git a/src/core/devices/tests/test-acd.c b/src/core/devices/tests/test-acd.c
new file mode 100644
index 00000000..b0af59db
--- /dev/null
+++ b/src/core/devices/tests/test-acd.c
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "n-acd/src/n-acd.h"
+
+#include <linux/if_ether.h>
+
+#include "devices/nm-acd-manager.h"
+#include "platform/tests/test-common.h"
+
+#define IFACE_VETH0 "nm-test-veth0"
+#define IFACE_VETH1 "nm-test-veth1"
+
+#define ADDR1 0x01010101
+#define ADDR2 0x02020202
+#define ADDR3 0x03030303
+#define ADDR4 0x04040404
+
+/*****************************************************************************/
+
+static gboolean
+_skip_acd_test_check(void)
+{
+    NAcd *       acd;
+    NAcdConfig * config;
+    const guint8 hwaddr[ETH_ALEN] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+    int          r;
+    static int   skip = -1;
+
+    if (skip == -1) {
+        r = n_acd_config_new(&config);
+        g_assert(r == 0);
+
+        n_acd_config_set_ifindex(config, 1);
+        n_acd_config_set_transport(config, N_ACD_TRANSPORT_ETHERNET);
+        n_acd_config_set_mac(config, hwaddr, sizeof(hwaddr));
+
+        r = n_acd_new(&acd, config);
+        n_acd_config_free(config);
+        if (r == 0)
+            n_acd_unref(acd);
+
+        skip = (r != 0);
+    }
+    return skip;
+}
+
+#define _skip_acd_test()                                               \
+    ({                                                                 \
+        gboolean _skip = _skip_acd_test_check();                       \
+                                                                       \
+        if (_skip)                                                     \
+            g_test_skip("Cannot create NAcd. Running under valgind?"); \
+        _skip;                                                         \
+    })
+
+/*****************************************************************************/
+
+typedef struct {
+    int           ifindex0;
+    int           ifindex1;
+    const guint8 *hwaddr0;
+    const guint8 *hwaddr1;
+    size_t        hwaddr0_len;
+    size_t        hwaddr1_len;
+} test_fixture;
+
+static void
+fixture_setup(test_fixture *fixture, gconstpointer user_data)
+{
+    /* create veth pair. */
+    fixture->ifindex0 =
+        nmtstp_link_veth_add(NM_PLATFORM_GET, -1, IFACE_VETH0, IFACE_VETH1)->ifindex;
+    fixture->ifindex1 =
+        nmtstp_link_get_typed(NM_PLATFORM_GET, -1, IFACE_VETH1, NM_LINK_TYPE_VETH)->ifindex;
+
+    g_assert(nm_platform_link_set_up(NM_PLATFORM_GET, fixture->ifindex0, NULL));
+    g_assert(nm_platform_link_set_up(NM_PLATFORM_GET, fixture->ifindex1, NULL));
+
+    fixture->hwaddr0 =
+        nm_platform_link_get_address(NM_PLATFORM_GET, fixture->ifindex0, &fixture->hwaddr0_len);
+    fixture->hwaddr1 =
+        nm_platform_link_get_address(NM_PLATFORM_GET, fixture->ifindex1, &fixture->hwaddr1_len);
+}
+
+typedef struct {
+    in_addr_t addresses[8];
+    in_addr_t peer_addresses[8];
+    gboolean  expected_result[8];
+} TestInfo;
+
+static void
+acd_manager_probe_terminated(NMAcdManager *acd_manager, gpointer user_data)
+{
+    g_main_loop_quit(user_data);
+}
+
+static void
+test_acd_common(test_fixture *fixture, TestInfo *info)
+{
+    nm_auto_free_acdmgr NMAcdManager *manager = NULL;
+    nm_auto_unref_gmainloop GMainLoop *loop   = NULL;
+    int                                i;
+    const guint                        WAIT_TIME_OPTIMISTIC = 50;
+    guint                              wait_time;
+    static const NMAcdCallbacks        callbacks = {
+        .probe_terminated_callback = acd_manager_probe_terminated,
+        .user_data_destroy         = (GDestroyNotify) g_main_loop_unref,
+    };
+    int r;
+
+    if (_skip_acd_test())
+        return;
+
+    /* first, try with a short waittime. We hope that this is long enough
+     * to successfully complete the test. Only if that's not the case, we
+     * assume the computer is currently busy (high load) and we retry with
+     * a longer timeout. */
+    wait_time = WAIT_TIME_OPTIMISTIC;
+again:
+
+    nm_clear_pointer(&loop, g_main_loop_unref);
+    loop = g_main_loop_new(NULL, FALSE);
+
+    nm_clear_pointer(&manager, nm_acd_manager_free);
+    manager = nm_acd_manager_new(fixture->ifindex0,
+                                 fixture->hwaddr0,
+                                 fixture->hwaddr0_len,
+                                 &callbacks,
+                                 g_main_loop_ref(loop));
+    g_assert(manager != NULL);
+
+    for (i = 0; info->addresses[i]; i++)
+        g_assert(nm_acd_manager_add_address(manager, info->addresses[i]));
+
+    for (i = 0; info->peer_addresses[i]; i++) {
+        nmtstp_ip4_address_add(NULL,
+                               FALSE,
+                               fixture->ifindex1,
+                               info->peer_addresses[i],
+                               24,
+                               0,
+                               3600,
+                               1800,
+                               0,
+                               NULL);
+    }
+
+    r = nm_acd_manager_start_probe(manager, wait_time);
+    g_assert_cmpint(r, ==, 0);
+
+    g_assert(nmtst_main_loop_run(loop, 2000));
+
+    for (i = 0; info->addresses[i]; i++) {
+        gboolean val;
+        char     sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+        val = nm_acd_manager_check_address(manager, info->addresses[i]);
+        if (val == info->expected_result[i])
+            continue;
+
+        if (wait_time == WAIT_TIME_OPTIMISTIC) {
+            /* probably we just had a glitch and the system took longer than
+             * expected. Re-verify with a large timeout this time. */
+            wait_time = 1000;
+            goto again;
+        }
+
+        g_error("expected check for address #%d (%s) to %s, but it didn't",
+                i,
+                _nm_utils_inet4_ntop(info->addresses[i], sbuf),
+                info->expected_result[i] ? "detect no duplicated" : "detect a duplicate");
+    }
+}
+
+static void
+test_acd_probe_1(test_fixture *fixture, gconstpointer user_data)
+{
+    TestInfo info = {.addresses       = {ADDR1, ADDR2, ADDR3},
+                     .peer_addresses  = {ADDR4},
+                     .expected_result = {TRUE, TRUE, TRUE}};
+
+    test_acd_common(fixture, &info);
+}
+
+static void
+test_acd_probe_2(test_fixture *fixture, gconstpointer user_data)
+{
+    TestInfo info = {.addresses       = {ADDR1, ADDR2, ADDR3, ADDR4},
+                     .peer_addresses  = {ADDR3, ADDR2},
+                     .expected_result = {TRUE, FALSE, FALSE, TRUE}};
+
+    test_acd_common(fixture, &info);
+}
+
+static void
+test_acd_announce(test_fixture *fixture, gconstpointer user_data)
+{
+    nm_auto_free_acdmgr NMAcdManager *manager = NULL;
+    nm_auto_unref_gmainloop GMainLoop *loop   = NULL;
+    int                                r;
+
+    if (_skip_acd_test())
+        return;
+
+    manager =
+        nm_acd_manager_new(fixture->ifindex0, fixture->hwaddr0, fixture->hwaddr0_len, NULL, NULL);
+    g_assert(manager != NULL);
+
+    g_assert(nm_acd_manager_add_address(manager, ADDR1));
+    g_assert(nm_acd_manager_add_address(manager, ADDR2));
+
+    loop = g_main_loop_new(NULL, FALSE);
+    r    = nm_acd_manager_announce_addresses(manager);
+    g_assert_cmpint(r, ==, 0);
+    g_assert(!nmtst_main_loop_run(loop, 200));
+}
+
+static void
+fixture_teardown(test_fixture *fixture, gconstpointer user_data)
+{
+    nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex0);
+    nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex1);
+}
+
+NMTstpSetupFunc const _nmtstp_setup_platform_func = nm_linux_platform_setup;
+
+void
+_nmtstp_init_tests(int *argc, char ***argv)
+{
+    nmtst_init_with_logging(argc, argv, NULL, "ALL");
+}
+
+void
+_nmtstp_setup_tests(void)
+{
+    g_test_add("/acd/probe/1",
+               test_fixture,
+               NULL,
+               fixture_setup,
+               test_acd_probe_1,
+               fixture_teardown);
+    g_test_add("/acd/probe/2",
+               test_fixture,
+               NULL,
+               fixture_setup,
+               test_acd_probe_2,
+               fixture_teardown);
+    g_test_add("/acd/announce",
+               test_fixture,
+               NULL,
+               fixture_setup,
+               test_acd_announce,
+               fixture_teardown);
+}
diff --git a/src/core/devices/tests/test-lldp.c b/src/core/devices/tests/test-lldp.c
new file mode 100644
index 00000000..ef0b1549
--- /dev/null
+++ b/src/core/devices/tests/test-lldp.c
@@ -0,0 +1,1316 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <fcntl.h>
+#include <netinet/if_ether.h>
+#include <linux/if_tun.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "devices/nm-lldp-listener.h"
+#include "systemd/nm-sd.h"
+
+#include "platform/tests/test-common.h"
+
+#include "nm-test-utils-core.h"
+
+/*****************************************************************************/
+
+static GVariant *
+get_lldp_neighbor(GVariant *  neighbors,
+                  int         chassis_id_type,
+                  const char *chassis_id,
+                  int         port_id_type,
+                  const char *port_id)
+{
+    GVariantIter iter;
+    GVariant *   variant;
+    GVariant *   result = NULL;
+
+    nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}"));
+
+    g_assert(chassis_id_type >= -1 && chassis_id_type <= G_MAXUINT8);
+    g_assert(port_id_type >= -1 && port_id_type <= G_MAXUINT8);
+
+    g_variant_iter_init(&iter, neighbors);
+    while (g_variant_iter_next(&iter, "@a{sv}", &variant)) {
+        gs_unref_variant GVariant *v_chassis_id_type = NULL;
+        gs_unref_variant GVariant *v_chassis_id      = NULL;
+        gs_unref_variant GVariant *v_port_id_type    = NULL;
+        gs_unref_variant GVariant *v_port_id         = NULL;
+
+        v_chassis_id_type =
+            g_variant_lookup_value(variant, NM_LLDP_ATTR_CHASSIS_ID_TYPE, G_VARIANT_TYPE_UINT32);
+        g_assert(v_chassis_id_type);
+
+        v_chassis_id =
+            g_variant_lookup_value(variant, NM_LLDP_ATTR_CHASSIS_ID, G_VARIANT_TYPE_STRING);
+        g_assert(v_chassis_id);
+
+        v_port_id_type =
+            g_variant_lookup_value(variant, NM_LLDP_ATTR_PORT_ID_TYPE, G_VARIANT_TYPE_UINT32);
+        g_assert(v_port_id_type);
+
+        v_port_id = g_variant_lookup_value(variant, NM_LLDP_ATTR_PORT_ID, G_VARIANT_TYPE_STRING);
+        g_assert(v_port_id);
+
+        if (nm_streq(g_variant_get_string(v_chassis_id, NULL), chassis_id)
+            && nm_streq(g_variant_get_string(v_port_id, NULL), port_id)
+            && NM_IN_SET(chassis_id_type, -1, g_variant_get_uint32(v_chassis_id_type))
+            && NM_IN_SET(port_id_type, -1, g_variant_get_uint32(v_port_id_type))) {
+            g_assert(!result);
+            result = variant;
+        } else
+            g_variant_unref(variant);
+    }
+
+    return result;
+}
+
+typedef struct {
+    int    ifindex;
+    int    fd;
+    guint8 mac[ETH_ALEN];
+} TestRecvFixture;
+
+typedef struct {
+    gsize          frame_len;
+    const uint8_t *frame;
+    const char *   as_variant;
+} TestRecvFrame;
+#define TEST_RECV_FRAME_DEFINE(name, _as_variant, ...)        \
+    static const guint8        _##name##_v[] = {__VA_ARGS__}; \
+    static const TestRecvFrame name          = {              \
+        .as_variant = _as_variant,                   \
+        .frame_len  = sizeof(_##name##_v),           \
+        .frame      = _##name##_v,                   \
+    }
+
+typedef struct {
+    guint                expected_num_called;
+    gsize                frames_len;
+    const TestRecvFrame *frames[10];
+    void (*check)(GMainLoop *loop, NMLldpListener *listener);
+} TestRecvData;
+#define TEST_RECV_DATA_DEFINE(name, _expected_num_called, _check, ...) \
+    static const TestRecvData name = {                                 \
+        .expected_num_called = _expected_num_called,                   \
+        .check               = _check,                                 \
+        .frames_len          = NM_NARG(__VA_ARGS__),                   \
+        .frames              = {__VA_ARGS__},                          \
+    }
+
+#define TEST_IFNAME "nm-tap-test0"
+
+TEST_RECV_FRAME_DEFINE(
+    _test_recv_data0_frame0,
+    "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x88, "
+    "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x04, 0x05, 0x31, 0x2f, "
+    "0x33, 0x06, 0x02, 0x00, 0x78, 0x08, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x0a, 0x03, 0x53, 0x59, "
+    "0x53, 0x0c, 0x04, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, "
+    "'chassis-id': <'00:01:02:03:04:05'>, 'port-id-type': <uint32 5>, 'port-id': <'1/3'>, "
+    "'destination': <'nearest-non-tpmr-bridge'>, 'port-description': <'Port'>, 'system-name': "
+    "<'SYS'>, 'system-description': <'foo'>}",
+    /* Ethernet header */
+    0x01,
+    0x80,
+    0xc2,
+    0x00,
+    0x00,
+    0x03, /* Destination MAC */
+    0x01,
+    0x02,
+    0x03,
+    0x04,
+    0x05,
+    0x06, /* Source MAC */
+    0x88,
+    0xcc, /* Ethertype */
+    /* LLDP mandatory TLVs */
+    0x02,
+    0x07,
+    0x04,
+    0x00,
+    0x01,
+    0x02, /* Chassis: MAC, 00:01:02:03:04:05 */
+    0x03,
+    0x04,
+    0x05,
+    0x04,
+    0x04,
+    0x05,
+    0x31,
+    0x2f,
+    0x33, /* Port: interface name, "1/3" */
+    0x06,
+    0x02,
+    0x00,
+    0x78, /* TTL: 120 seconds */
+    /* LLDP optional TLVs */
+    0x08,
+    0x04,
+    0x50,
+    0x6f,
+    0x72,
+    0x74, /* Port Description: "Port" */
+    0x0a,
+    0x03,
+    0x53,
+    0x59,
+    0x53, /* System Name: "SYS" */
+    0x0c,
+    0x04,
+    0x66,
+    0x6f,
+    0x6f,
+    0x00, /* System Description: "foo" (NULL-terminated) */
+    0x00,
+    0x00 /* End Of LLDPDU */
+);
+
+static void
+_test_recv_data0_check_do(GMainLoop *loop, NMLldpListener *listener, const TestRecvFrame *frame)
+{
+    GVariant *       neighbors, *attr;
+    gs_unref_variant GVariant *neighbor = NULL;
+
+    neighbors = nm_lldp_listener_get_neighbors(listener);
+    nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}"));
+    g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
+
+    neighbor = get_lldp_neighbor(neighbors,
+                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 "00:01:02:03:04:05",
+                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 "1/3");
+    g_assert(neighbor);
+    g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 4);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING);
+    nmtst_assert_variant_bytestring(attr, frame->frame, frame->frame_len);
+    nm_clear_g_variant(&attr);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_PORT_DESCRIPTION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "Port");
+    nm_clear_g_variant(&attr);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_NAME, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "SYS");
+    nm_clear_g_variant(&attr);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_DESTINATION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE);
+    nm_clear_g_variant(&attr);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "foo");
+    nm_clear_g_variant(&attr);
+}
+
+static void
+_test_recv_data0_check(GMainLoop *loop, NMLldpListener *listener)
+{
+    _test_recv_data0_check_do(loop, listener, &_test_recv_data0_frame0);
+}
+
+TEST_RECV_DATA_DEFINE(_test_recv_data0, 1, _test_recv_data0_check, &_test_recv_data0_frame0);
+TEST_RECV_DATA_DEFINE(_test_recv_data0_twice,
+                      1,
+                      _test_recv_data0_check,
+                      &_test_recv_data0_frame0,
+                      &_test_recv_data0_frame0);
+
+TEST_RECV_FRAME_DEFINE(
+    _test_recv_data1_frame0,
+    /* lldp.detailed.pcap from
+     * https://wiki.wireshark.org/SampleCaptures#Link_Layer_Discovery_Protocol_.28LLDP.29 */
+    "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x01, 0x30, 0xf9, 0xad, 0xa0, 0x88, "
+    "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x30, 0xf9, 0xad, 0xa0, 0x04, 0x04, 0x05, 0x31, 0x2f, "
+    "0x31, 0x06, 0x02, 0x00, 0x78, 0x08, 0x17, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, "
+    "0x30, 0x2d, 0x34, 0x38, 0x2d, 0x50, 0x6f, 0x72, 0x74, 0x20, 0x31, 0x30, 0x30, 0x31, 0x00, "
+    "0x0a, 0x0d, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, 0x30, 0x2d, 0x34, 0x38, 0x00, "
+    "0x0c, 0x4c, 0x53, 0x75, 0x6d, 0x6d, 0x69, 0x74, 0x33, 0x30, 0x30, 0x2d, 0x34, 0x38, 0x20, "
+    "0x2d, 0x20, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x37, 0x2e, 0x34, 0x65, 0x2e, "
+    "0x31, 0x20, 0x28, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x20, 0x35, 0x29, 0x20, 0x62, 0x79, 0x20, "
+    "0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x5f, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, "
+    "0x30, 0x35, 0x2f, 0x32, 0x37, 0x2f, 0x30, 0x35, 0x20, 0x30, 0x34, 0x3a, 0x35, 0x33, 0x3a, "
+    "0x31, 0x31, 0x00, 0x0e, 0x04, 0x00, 0x14, 0x00, 0x14, 0x10, 0x0e, 0x07, 0x06, 0x00, 0x01, "
+    "0x30, 0xf9, 0xad, 0xa0, 0x02, 0x00, 0x00, 0x03, 0xe9, 0x00, 0xfe, 0x07, 0x00, 0x12, 0x0f, "
+    "0x02, 0x07, 0x01, 0x00, 0xfe, 0x09, 0x00, 0x12, 0x0f, 0x01, 0x03, 0x6c, 0x00, 0x00, 0x10, "
+    "0xfe, 0x09, 0x00, 0x12, 0x0f, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x06, 0x00, 0x12, "
+    "0x0f, 0x04, 0x05, 0xf2, 0xfe, 0x06, 0x00, 0x80, 0xc2, 0x01, 0x01, 0xe8, 0xfe, 0x07, 0x00, "
+    "0x80, 0xc2, 0x02, 0x01, 0x00, 0x00, 0xfe, 0x16, 0x00, 0x80, 0xc2, 0x03, 0x01, 0xe8, 0x0f, "
+    "0x76, 0x32, 0x2d, 0x30, 0x34, 0x38, 0x38, 0x2d, 0x30, 0x33, 0x2d, 0x30, 0x35, 0x30, 0x35, "
+    "0xfe, 0x05, 0x00, 0x80, 0xc2, 0x04, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, "
+    "'chassis-id': <'00:01:30:F9:AD:A0'>, 'port-id-type': <uint32 5>, 'port-id': <'1/1'>, "
+    "'destination': <'nearest-bridge'>, 'port-description': <'Summit300-48-Port 1001'>, "
+    "'system-name': <'Summit300-48'>, 'system-description': <'Summit300-48 - Version 7.4e.1 (Build "
+    "5) by Release_Master 05/27/05 04:53:11'>, 'system-capabilities': <uint32 20>, "
+    "'management-addresses': <[{'address-subtype': <uint32 6>, 'address': <[byte 0x00, 0x01, 0x30, "
+    "0xf9, 0xad, 0xa0]>, 'interface-number-subtype': <uint32 2>, 'interface-number': <uint32 "
+    "1001>}]>, 'ieee-802-1-pvid': <uint32 488>, 'ieee-802-1-ppvid': <uint32 0>, "
+    "'ieee-802-1-ppvid-flags': <uint32 1>, 'ieee-802-1-ppvids': <[{'ppvid': <uint32 0>, 'flags': "
+    "<uint32 1>}]>, 'ieee-802-1-vid': <uint32 488>, 'ieee-802-1-vlan-name': <'v2-0488-03-0505'>, "
+    "'ieee-802-1-vlans': <[{'vid': <uint32 488>, 'name': <'v2-0488-03-0505'>}]>, "
+    "'ieee-802-3-mac-phy-conf': <{'autoneg': <uint32 3>, 'pmd-autoneg-cap': <uint32 27648>, "
+    "'operational-mau-type': <uint32 16>}>, 'ieee-802-3-power-via-mdi': <{'mdi-power-support': "
+    "<uint32 7>, 'pse-power-pair': <uint32 1>, 'power-class': <uint32 0>}>, "
+    "'ieee-802-3-max-frame-size': <uint32 1522>}",
+    /* ethernet header */
+    0x01,
+    0x80,
+    0xc2,
+    0x00,
+    0x00,
+    0x0e, /* destination mac */
+    0x00,
+    0x01,
+    0x30,
+    0xf9,
+    0xad,
+    0xa0, /* source mac */
+    0x88,
+    0xcc, /* ethernet type */
+
+    0x02,
+    0x07,
+    0x04,
+    0x00,
+    0x01,
+    0x30, /* Chassis Subtype */
+    0xf9,
+    0xad,
+    0xa0,
+    0x04,
+    0x04,
+    0x05,
+    0x31,
+    0x2f,
+    0x31, /* Port Subtype */
+    0x06,
+    0x02,
+    0x00,
+    0x78, /* Time To Live */
+    0x08,
+    0x17,
+    0x53,
+    0x75,
+    0x6d,
+    0x6d, /* Port Description */
+    0x69,
+    0x74,
+    0x33,
+    0x30,
+    0x30,
+    0x2d,
+    0x34,
+    0x38,
+    0x2d,
+    0x50,
+    0x6f,
+    0x72,
+    0x74,
+    0x20,
+    0x31,
+    0x30,
+    0x30,
+    0x31,
+    0x00,
+    0x0a,
+    0x0d,
+    0x53,
+    0x75,
+    0x6d,
+    0x6d, /* System Name */
+    0x69,
+    0x74,
+    0x33,
+    0x30,
+    0x30,
+    0x2d,
+    0x34,
+    0x38,
+    0x00,
+    0x0c,
+    0x4c,
+    0x53,
+    0x75,
+    0x6d,
+    0x6d, /* System Description */
+    0x69,
+    0x74,
+    0x33,
+    0x30,
+    0x30,
+    0x2d,
+    0x34,
+    0x38,
+    0x20,
+    0x2d,
+    0x20,
+    0x56,
+    0x65,
+    0x72,
+    0x73,
+    0x69,
+    0x6f,
+    0x6e,
+    0x20,
+    0x37,
+    0x2e,
+    0x34,
+    0x65,
+    0x2e,
+    0x31,
+    0x20,
+    0x28,
+    0x42,
+    0x75,
+    0x69,
+    0x6c,
+    0x64,
+    0x20,
+    0x35,
+    0x29,
+    0x20,
+    0x62,
+    0x79,
+    0x20,
+    0x52,
+    0x65,
+    0x6c,
+    0x65,
+    0x61,
+    0x73,
+    0x65,
+    0x5f,
+    0x4d,
+    0x61,
+    0x73,
+    0x74,
+    0x65,
+    0x72,
+    0x20,
+    0x30,
+    0x35,
+    0x2f,
+    0x32,
+    0x37,
+    0x2f,
+    0x30,
+    0x35,
+    0x20,
+    0x30,
+    0x34,
+    0x3a,
+    0x35,
+    0x33,
+    0x3a,
+    0x31,
+    0x31,
+    0x00,
+    0x0e,
+    0x04,
+    0x00,
+    0x14,
+    0x00,
+    0x14, /* Capabilities */
+    0x10,
+    0x0e,
+    0x07,
+    0x06,
+    0x00,
+    0x01, /* Management Address */
+    0x30,
+    0xf9,
+    0xad,
+    0xa0,
+    0x02,
+    0x00,
+    0x00,
+    0x03,
+    0xe9,
+    0x00,
+    0xfe,
+    0x07,
+    0x00,
+    0x12,
+    0x0f,
+    0x02, /* IEEE 802.3 - Power Via MDI */
+    0x07,
+    0x01,
+    0x00,
+    0xfe,
+    0x09,
+    0x00,
+    0x12,
+    0x0f,
+    0x01, /* IEEE 802.3 - MAC/PHY Configuration/Status */
+    0x03,
+    0x6c,
+    0x00,
+    0x00,
+    0x10,
+    0xfe,
+    0x09,
+    0x00,
+    0x12,
+    0x0f,
+    0x03, /* IEEE 802.3 - Link Aggregation */
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xfe,
+    0x06,
+    0x00,
+    0x12,
+    0x0f,
+    0x04, /* IEEE 802.3 - Maximum Frame Size */
+    0x05,
+    0xf2,
+    0xfe,
+    0x06,
+    0x00,
+    0x80,
+    0xc2,
+    0x01, /* IEEE 802.1 - Port VLAN ID */
+    0x01,
+    0xe8,
+    0xfe,
+    0x07,
+    0x00,
+    0x80,
+    0xc2,
+    0x02, /* IEEE 802.1 - Port and Protocol VLAN ID */
+    0x01,
+    0x00,
+    0x00,
+    0xfe,
+    0x16,
+    0x00,
+    0x80,
+    0xc2,
+    0x03, /* IEEE 802.1 - VLAN Name */
+    0x01,
+    0xe8,
+    0x0f,
+    0x76,
+    0x32,
+    0x2d,
+    0x30,
+    0x34,
+    0x38,
+    0x38,
+    0x2d,
+    0x30,
+    0x33,
+    0x2d,
+    0x30,
+    0x35,
+    0x30,
+    0x35,
+    0xfe,
+    0x05,
+    0x00,
+    0x80,
+    0xc2,
+    0x04, /* IEEE 802.1 - Protocol Identity */
+    0x00,
+    0x00,
+    0x00 /* End of LLDPDU */
+);
+
+static void
+_test_recv_data1_check(GMainLoop *loop, NMLldpListener *listener)
+{
+    GVariant *       neighbors, *attr, *child;
+    gs_unref_variant GVariant *neighbor = NULL;
+    guint                      v_uint   = 0;
+    const char *               v_str    = NULL;
+
+    neighbors = nm_lldp_listener_get_neighbors(listener);
+    nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}"));
+    g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
+
+    neighbor = get_lldp_neighbor(neighbors,
+                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 "00:01:30:F9:AD:A0",
+                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 "1/1");
+    g_assert(neighbor);
+    g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 16);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING);
+    nmtst_assert_variant_bytestring(attr,
+                                    _test_recv_data1_frame0.frame,
+                                    _test_recv_data1_frame0.frame_len);
+    nm_clear_g_variant(&attr);
+
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_DESTINATION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, NM_LLDP_DEST_NEAREST_BRIDGE);
+    nm_clear_g_variant(&attr);
+
+    /* unsupported: Time To Live */
+
+    /* Port Description */
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_PORT_DESCRIPTION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "Summit300-48-Port 1001");
+    nm_clear_g_variant(&attr);
+
+    /* System Name */
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_NAME, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "Summit300-48");
+    nm_clear_g_variant(&attr);
+
+    /* System Description */
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(
+        attr,
+        "Summit300-48 - Version 7.4e.1 (Build 5) by Release_Master 05/27/05 04:53:11");
+    nm_clear_g_variant(&attr);
+
+    /* Capabilities */
+    attr =
+        g_variant_lookup_value(neighbor, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 20);
+    nm_clear_g_variant(&attr);
+
+    /* Management Address */
+    attr = g_variant_lookup_value(neighbor,
+                                  NM_LLDP_ATTR_MANAGEMENT_ADDRESSES,
+                                  G_VARIANT_TYPE("aa{sv}"));
+    g_assert(attr);
+    g_assert_cmpuint(g_variant_n_children(attr), ==, 1);
+    child = g_variant_get_child_value(attr, 0);
+    g_assert(child);
+    g_assert(g_variant_lookup(child, "interface-number", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 1001);
+    g_assert(g_variant_lookup(child, "interface-number-subtype", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 2);
+    g_assert(g_variant_lookup(child, "address-subtype", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 6);
+    nm_clear_g_variant(&child);
+    nm_clear_g_variant(&attr);
+
+    /* IEEE 802.3 - Power Via MDI */
+    attr = g_variant_lookup_value(neighbor,
+                                  NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI,
+                                  G_VARIANT_TYPE_VARDICT);
+    g_assert(attr);
+    g_assert(g_variant_lookup(attr, "mdi-power-support", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 7);
+    g_assert(g_variant_lookup(attr, "pse-power-pair", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 1);
+    g_assert(g_variant_lookup(attr, "power-class", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 0);
+    nm_clear_g_variant(&attr);
+
+    /* IEEE 802.3 - MAC/PHY Configuration/Status */
+    attr = g_variant_lookup_value(neighbor,
+                                  NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF,
+                                  G_VARIANT_TYPE_VARDICT);
+    g_assert(attr);
+    g_assert(g_variant_lookup(attr, "autoneg", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 3);
+    g_assert(g_variant_lookup(attr, "pmd-autoneg-cap", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 0x6c00);
+    g_assert(g_variant_lookup(attr, "operational-mau-type", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 16);
+    nm_clear_g_variant(&attr);
+
+    /* unsupported: IEEE 802.3 - Link Aggregation */
+
+    /* Maximum Frame Size */
+    attr = g_variant_lookup_value(neighbor,
+                                  NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE,
+                                  G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 1522);
+    nm_clear_g_variant(&attr);
+
+    /* IEEE 802.1 - Port VLAN ID */
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PVID, G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 488);
+    nm_clear_g_variant(&attr);
+
+    /* IEEE 802.1 - Port and Protocol VLAN ID */
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PPVID, G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 0);
+    nm_clear_g_variant(&attr);
+    attr = g_variant_lookup_value(neighbor,
+                                  NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS,
+                                  G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 1);
+    nm_clear_g_variant(&attr);
+
+    /* new PPVID attributes */
+    attr =
+        g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_PPVIDS, G_VARIANT_TYPE("aa{sv}"));
+    g_assert_cmpuint(g_variant_n_children(attr), ==, 1);
+    child = g_variant_get_child_value(attr, 0);
+    g_assert(child);
+    g_assert(g_variant_lookup(child, "ppvid", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 0);
+    g_assert(g_variant_lookup(child, "flags", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 1);
+    nm_clear_g_variant(&child);
+    nm_clear_g_variant(&attr);
+
+    /* IEEE 802.1 - VLAN Name */
+    attr =
+        g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME, G_VARIANT_TYPE_STRING);
+    nmtst_assert_variant_string(attr, "v2-0488-03-0505");
+    nm_clear_g_variant(&attr);
+    attr = g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VID, G_VARIANT_TYPE_UINT32);
+    nmtst_assert_variant_uint32(attr, 488);
+    nm_clear_g_variant(&attr);
+
+    /* new VLAN attributes */
+    attr =
+        g_variant_lookup_value(neighbor, NM_LLDP_ATTR_IEEE_802_1_VLANS, G_VARIANT_TYPE("aa{sv}"));
+    g_assert_cmpuint(g_variant_n_children(attr), ==, 1);
+    child = g_variant_get_child_value(attr, 0);
+    g_assert(child);
+    g_assert(g_variant_lookup(child, "vid", "u", &v_uint));
+    g_assert_cmpint(v_uint, ==, 488);
+    g_assert(g_variant_lookup(child, "name", "&s", &v_str));
+    g_assert_cmpstr(v_str, ==, "v2-0488-03-0505");
+    nm_clear_g_variant(&child);
+    nm_clear_g_variant(&attr);
+
+    /* unsupported: IEEE 802.1 - Protocol Identity */
+}
+
+TEST_RECV_DATA_DEFINE(_test_recv_data1, 1, _test_recv_data1_check, &_test_recv_data1_frame0);
+
+TEST_RECV_FRAME_DEFINE(
+    _test_recv_data2_frame0_ttl1,
+    "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x03, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x88, "
+    "0xcc, 0x02, 0x07, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x04, 0x04, 0x05, 0x31, 0x2f, "
+    "0x33, 0x06, 0x02, 0x00, 0x01, 0x08, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x0a, 0x03, 0x53, 0x59, "
+    "0x53, 0x0c, 0x04, 0x66, 0x6f, 0x6f, 0x00, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, "
+    "'chassis-id': <'00:01:02:03:04:05'>, 'port-id-type': <uint32 5>, 'port-id': <'1/3'>, "
+    "'destination': <'nearest-non-tpmr-bridge'>, 'port-description': <'Port'>, 'system-name': "
+    "<'SYS'>, 'system-description': <'foo'>}",
+    /* Ethernet header */
+    0x01,
+    0x80,
+    0xc2,
+    0x00,
+    0x00,
+    0x03, /* Destination MAC */
+    0x01,
+    0x02,
+    0x03,
+    0x04,
+    0x05,
+    0x06, /* Source MAC */
+    0x88,
+    0xcc, /* Ethertype */
+    /* LLDP mandatory TLVs */
+    0x02,
+    0x07,
+    0x04,
+    0x00,
+    0x01,
+    0x02, /* Chassis: MAC, 00:01:02:03:04:05 */
+    0x03,
+    0x04,
+    0x05,
+    0x04,
+    0x04,
+    0x05,
+    0x31,
+    0x2f,
+    0x33, /* Port: interface name, "1/3" */
+    0x06,
+    0x02,
+    0x00,
+    0x01, /* TTL: 1 seconds */
+    /* LLDP optional TLVs */
+    0x08,
+    0x04,
+    0x50,
+    0x6f,
+    0x72,
+    0x74, /* Port Description: "Port" */
+    0x0a,
+    0x03,
+    0x53,
+    0x59,
+    0x53, /* System Name: "SYS" */
+    0x0c,
+    0x04,
+    0x66,
+    0x6f,
+    0x6f,
+    0x00, /* System Description: "foo" (NULL-terminated) */
+    0x00,
+    0x00 /* End Of LLDPDU */
+);
+
+static void
+_test_recv_data2_ttl1_check(GMainLoop *loop, NMLldpListener *listener)
+{
+    gulong    notify_id;
+    GVariant *neighbors;
+
+    _test_recv_data0_check_do(loop, listener, &_test_recv_data2_frame0_ttl1);
+
+    /* wait for signal. */
+    notify_id = g_signal_connect(listener,
+                                 "notify::" NM_LLDP_LISTENER_NEIGHBORS,
+                                 nmtst_main_loop_quit_on_notify,
+                                 loop);
+    if (!nmtst_main_loop_run(loop, 5000))
+        g_assert_not_reached();
+    nm_clear_g_signal_handler(listener, &notify_id);
+
+    neighbors = nm_lldp_listener_get_neighbors(listener);
+    nmtst_assert_variant_is_of_type(neighbors, G_VARIANT_TYPE("aa{sv}"));
+    g_assert_cmpint(g_variant_n_children(neighbors), ==, 0);
+}
+
+TEST_RECV_DATA_DEFINE(_test_recv_data2_ttl1,
+                      1,
+                      _test_recv_data2_ttl1_check,
+                      &_test_recv_data2_frame0_ttl1);
+
+static void
+_test_recv_fixture_setup(TestRecvFixture *fixture, gconstpointer user_data)
+{
+    const NMPlatformLink *link;
+    nm_auto_close int     fd = -1;
+
+    fd = open("/dev/net/tun", O_RDWR | O_CLOEXEC);
+    if (fd == -1) {
+        g_test_skip("Unable to open /dev/net/tun");
+        fixture->ifindex = 0;
+        return;
+    }
+
+    if (nmtst_get_rand_bool()) {
+        const NMPlatformLnkTun lnk = {
+            .type        = IFF_TAP,
+            .pi          = FALSE,
+            .vnet_hdr    = FALSE,
+            .multi_queue = FALSE,
+            .persist     = FALSE,
+        };
+
+        nm_close(nm_steal_fd(&fd));
+
+        link = nmtstp_link_tun_add(NM_PLATFORM_GET, FALSE, TEST_IFNAME, &lnk, &fd);
+        g_assert(link);
+        nmtstp_link_set_updown(NM_PLATFORM_GET, -1, link->ifindex, TRUE);
+        link = nmtstp_assert_wait_for_link(NM_PLATFORM_GET, TEST_IFNAME, NM_LINK_TYPE_TUN, 0);
+    } else {
+        int          s;
+        struct ifreq ifr;
+        int          r;
+        int          try_cnt = 0;
+
+again:
+        memset(&ifr, 0, sizeof(ifr));
+        ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
+        nm_utils_ifname_cpy(ifr.ifr_name, TEST_IFNAME);
+
+        r = ioctl(fd, TUNSETIFF, &ifr);
+        if (r != 0) {
+            if (errno == EPERM && try_cnt++ < 10) {
+                g_usleep(2);
+                goto again;
+            }
+            g_assert_cmpint(errno, ==, 0);
+            g_assert_cmpint(r, ==, 0);
+        }
+
+        /* Bring the interface up */
+        s = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+        g_assert(s >= 0);
+
+        ifr.ifr_flags |= IFF_UP;
+        r = ioctl(s, SIOCSIFFLAGS, &ifr);
+        if (r != 0) {
+            g_assert_cmpint(errno, ==, 0);
+            g_assert_cmpint(r, ==, 0);
+        }
+
+        nm_close(s);
+
+        link = nmtstp_assert_wait_for_link(NM_PLATFORM_GET, TEST_IFNAME, NM_LINK_TYPE_TUN, 100);
+    }
+
+    fixture->ifindex = link->ifindex;
+    fixture->fd      = nm_steal_fd(&fd);
+    memcpy(fixture->mac, link->l_address.data, ETH_ALEN);
+}
+
+typedef struct {
+    int num_called;
+} TestRecvCallbackInfo;
+
+static void
+lldp_neighbors_changed(NMLldpListener *lldp_listener, GParamSpec *pspec, gpointer user_data)
+{
+    TestRecvCallbackInfo *info = user_data;
+
+    info->num_called++;
+}
+
+static void
+test_recv(TestRecvFixture *fixture, gconstpointer user_data)
+{
+    const TestRecvData *data                 = user_data;
+    gs_unref_object NMLldpListener *listener = NULL;
+    GMainLoop *                     loop;
+    TestRecvCallbackInfo            info = {};
+    gsize                           i_frames;
+    gulong                          notify_id;
+    GError *                        error = NULL;
+    guint                           sd_id;
+
+    if (fixture->ifindex == 0) {
+        g_test_skip("Tun device not available");
+        return;
+    }
+
+    listener = nm_lldp_listener_new();
+    g_assert(listener != NULL);
+    g_assert(nm_lldp_listener_start(listener, fixture->ifindex, &error));
+    g_assert_no_error(error);
+
+    notify_id = g_signal_connect(listener,
+                                 "notify::" NM_LLDP_LISTENER_NEIGHBORS,
+                                 (GCallback) lldp_neighbors_changed,
+                                 &info);
+    loop      = g_main_loop_new(NULL, FALSE);
+    sd_id     = nm_sd_event_attach_default();
+
+    for (i_frames = 0; i_frames < data->frames_len; i_frames++) {
+        const TestRecvFrame *f = data->frames[i_frames];
+
+        g_assert(write(fixture->fd, f->frame, f->frame_len) == f->frame_len);
+    }
+
+    if (nmtst_main_loop_run(loop, 500))
+        g_assert_not_reached();
+
+    g_assert_cmpint(info.num_called, ==, data->expected_num_called);
+
+    nm_clear_g_signal_handler(listener, &notify_id);
+
+    data->check(loop, listener);
+
+    nm_clear_g_source(&sd_id);
+    nm_clear_pointer(&loop, g_main_loop_unref);
+}
+
+static void
+_test_recv_fixture_teardown(TestRecvFixture *fixture, gconstpointer user_data)
+{
+    if (fixture->ifindex)
+        nm_platform_link_delete(NM_PLATFORM_GET, fixture->ifindex);
+}
+
+/*****************************************************************************/
+
+static void
+test_parse_frames(gconstpointer test_data)
+{
+    const TestRecvFrame *frame            = test_data;
+    gs_unref_variant GVariant *v_neighbor = NULL;
+    gs_unref_variant GVariant *attr       = NULL;
+    gs_free char *             as_variant = NULL;
+
+    v_neighbor = nmtst_lldp_parse_from_raw(frame->frame, frame->frame_len);
+    g_assert(v_neighbor);
+
+    attr = g_variant_lookup_value(v_neighbor, NM_LLDP_ATTR_RAW, G_VARIANT_TYPE_BYTESTRING);
+    nmtst_assert_variant_bytestring(attr, frame->frame, frame->frame_len);
+    nm_clear_g_variant(&attr);
+
+    as_variant = g_variant_print(v_neighbor, TRUE);
+    g_assert(as_variant);
+    g_assert_cmpstr(frame->as_variant, ==, as_variant);
+}
+
+/*****************************************************************************/
+
+TEST_RECV_FRAME_DEFINE(
+    _test_parse_frames_3,
+    /* https://github.com/the-tcpdump-group/tcpdump/blob/c4f8796bf8bec740621a360eded236d8991ea00f/tests/lldp_mudurl.pcap */
+    "{'raw': <[byte 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x23, 0x54, 0xc2, 0x57, 0x02, 0x88, "
+    "0xcc, 0x02, 0x07, 0x04, 0x00, 0x23, 0x54, 0xc2, 0x57, 0x02, 0x04, 0x07, 0x03, 0x00, 0x23, "
+    "0x54, 0xc2, 0x57, 0x02, 0x06, 0x02, 0x00, 0x78, 0x0a, 0x1c, 0x75, 0x70, 0x73, 0x74, 0x61, "
+    "0x69, 0x72, 0x73, 0x2e, 0x6f, 0x66, 0x63, 0x6f, 0x75, 0x72, 0x73, 0x65, 0x69, 0x6d, 0x72, "
+    "0x69, 0x67, 0x68, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x0c, 0x5c, 0x55, 0x62, 0x75, 0x6e, 0x74, "
+    "0x75, 0x20, 0x31, 0x34, 0x2e, 0x30, 0x34, 0x2e, 0x35, 0x20, 0x4c, 0x54, 0x53, 0x20, 0x4c, "
+    "0x69, 0x6e, 0x75, 0x78, 0x20, 0x33, 0x2e, 0x31, 0x33, 0x2e, 0x30, 0x2d, 0x31, 0x30, 0x36, "
+    "0x2d, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x20, 0x23, 0x31, 0x35, 0x33, 0x2d, 0x55, "
+    "0x62, 0x75, 0x6e, 0x74, 0x75, 0x20, 0x53, 0x4d, 0x50, 0x20, 0x54, 0x75, 0x65, 0x20, 0x44, "
+    "0x65, 0x63, 0x20, 0x36, 0x20, 0x31, 0x35, 0x3a, 0x34, 0x35, 0x3a, 0x31, 0x33, 0x20, 0x55, "
+    "0x54, 0x43, 0x20, 0x32, 0x30, 0x31, 0x36, 0x20, 0x69, 0x36, 0x38, 0x36, 0x0e, 0x04, 0x00, "
+    "0x9c, 0x00, 0x08, 0x10, 0x0c, 0x05, 0x01, 0x3e, 0x0c, 0xad, 0x72, 0x02, 0x00, 0x00, 0x00, "
+    "0x02, 0x00, 0x10, 0x18, 0x11, 0x02, 0x20, 0x01, 0x08, 0xa8, 0x10, 0x06, 0x00, 0x04, 0x02, "
+    "0x23, 0x54, 0xff, 0xfe, 0xc2, 0x57, 0x02, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x08, 0x04, "
+    "0x65, 0x74, 0x68, 0x30, 0xfe, 0x09, 0x00, 0x12, 0x0f, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, "
+    "0xfe, 0x09, 0x00, 0x12, 0x0f, 0x01, 0x03, 0xec, 0xc3, 0x00, 0x10, 0xfe, 0x40, 0x00, 0x00, "
+    "0x5e, 0x01, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x69, 0x6d, 0x72, 0x69, 0x67, "
+    "0x68, 0x74, 0x2e, 0x6d, 0x75, 0x64, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, "
+    "0x63, 0x6f, 0x6d, 0x2f, 0x2e, 0x77, 0x65, 0x6c, 0x6c, 0x2d, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, "
+    "0x2f, 0x6d, 0x75, 0x64, 0x2f, 0x76, 0x31, 0x2f, 0x76, 0x6f, 0x6d, 0x69, 0x74, 0x76, 0x32, "
+    "0x2e, 0x30, 0x00, 0x00]>, 'chassis-id-type': <uint32 4>, 'chassis-id': <'00:23:54:C2:57:02'>, "
+    "'port-id-type': <uint32 3>, 'port-id': <'00:23:54:C2:57:02'>, 'destination': "
+    "<'nearest-bridge'>, 'port-description': <'eth0'>, 'system-name': "
+    "<'upstairs.ofcourseimright.com'>, 'system-description': <'Ubuntu 14.04.5 LTS Linux "
+    "3.13.0-106-generic #153-Ubuntu SMP Tue Dec 6 15:45:13 UTC 2016 i686'>, 'system-capabilities': "
+    "<uint32 156>, 'management-addresses': <[{'address-subtype': <uint32 1>, 'address': <[byte "
+    "0x3e, 0x0c, 0xad, 0x72]>, 'interface-number-subtype': <uint32 2>, 'interface-number': <uint32 "
+    "2>}, {'address-subtype': <uint32 2>, 'address': <[byte 0x20, 0x01, 0x08, 0xa8, 0x10, 0x06, "
+    "0x00, 0x04, 0x02, 0x23, 0x54, 0xff, 0xfe, 0xc2, 0x57, 0x02]>, 'interface-number-subtype': "
+    "<uint32 2>, 'interface-number': <uint32 2>}]>, 'ieee-802-3-mac-phy-conf': <{'autoneg': "
+    "<uint32 3>, 'pmd-autoneg-cap': <uint32 60611>, 'operational-mau-type': <uint32 16>}>, "
+    "'mud-url': <'https://imright.mud.example.com/.well-known/mud/v1/vomitv2.0'>}",
+    0x01,
+    0x80,
+    0xc2,
+    0x00,
+    0x00,
+    0x0e, /* ethernet destination */
+    0x00,
+    0x23,
+    0x54,
+    0xc2,
+    0x57,
+    0x02, /* ethernet source */
+    0x88,
+    0xcc, /* ethernet type */
+
+    0x02,
+    0x07,
+    0x04,
+    0x00,
+    0x23,
+    0x54,
+    0xc2,
+    0x57,
+    0x02,
+    0x04,
+    0x07,
+    0x03,
+    0x00,
+    0x23,
+    0x54,
+    0xc2,
+    0x57,
+    0x02,
+    0x06,
+    0x02,
+    0x00,
+    0x78,
+    0x0a,
+    0x1c,
+    0x75,
+    0x70,
+    0x73,
+    0x74,
+    0x61,
+    0x69,
+    0x72,
+    0x73,
+    0x2e,
+    0x6f,
+    0x66,
+    0x63,
+    0x6f,
+    0x75,
+    0x72,
+    0x73,
+    0x65,
+    0x69,
+    0x6d,
+    0x72,
+    0x69,
+    0x67,
+    0x68,
+    0x74,
+    0x2e,
+    0x63,
+    0x6f,
+    0x6d,
+    0x0c,
+    0x5c,
+    0x55,
+    0x62,
+    0x75,
+    0x6e,
+    0x74,
+    0x75,
+    0x20,
+    0x31,
+    0x34,
+    0x2e,
+    0x30,
+    0x34,
+    0x2e,
+    0x35,
+    0x20,
+    0x4c,
+    0x54,
+    0x53,
+    0x20,
+    0x4c,
+    0x69,
+    0x6e,
+    0x75,
+    0x78,
+    0x20,
+    0x33,
+    0x2e,
+    0x31,
+    0x33,
+    0x2e,
+    0x30,
+    0x2d,
+    0x31,
+    0x30,
+    0x36,
+    0x2d,
+    0x67,
+    0x65,
+    0x6e,
+    0x65,
+    0x72,
+    0x69,
+    0x63,
+    0x20,
+    0x23,
+    0x31,
+    0x35,
+    0x33,
+    0x2d,
+    0x55,
+    0x62,
+    0x75,
+    0x6e,
+    0x74,
+    0x75,
+    0x20,
+    0x53,
+    0x4d,
+    0x50,
+    0x20,
+    0x54,
+    0x75,
+    0x65,
+    0x20,
+    0x44,
+    0x65,
+    0x63,
+    0x20,
+    0x36,
+    0x20,
+    0x31,
+    0x35,
+    0x3a,
+    0x34,
+    0x35,
+    0x3a,
+    0x31,
+    0x33,
+    0x20,
+    0x55,
+    0x54,
+    0x43,
+    0x20,
+    0x32,
+    0x30,
+    0x31,
+    0x36,
+    0x20,
+    0x69,
+    0x36,
+    0x38,
+    0x36,
+    0x0e,
+    0x04,
+    0x00,
+    0x9c,
+    0x00,
+    0x08,
+    0x10,
+    0x0c,
+    0x05,
+    0x01,
+    0x3e,
+    0x0c,
+    0xad,
+    0x72,
+    0x02,
+    0x00,
+    0x00,
+    0x00,
+    0x02,
+    0x00,
+    0x10,
+    0x18,
+    0x11,
+    0x02,
+    0x20,
+    0x01,
+    0x08,
+    0xa8,
+    0x10,
+    0x06,
+    0x00,
+    0x04,
+    0x02,
+    0x23,
+    0x54,
+    0xff,
+    0xfe,
+    0xc2,
+    0x57,
+    0x02,
+    0x02,
+    0x00,
+    0x00,
+    0x00,
+    0x02,
+    0x00,
+    0x08,
+    0x04,
+    0x65,
+    0x74,
+    0x68,
+    0x30,
+    0xfe,
+    0x09,
+    0x00,
+    0x12,
+    0x0f,
+    0x03,
+    0x01,
+    0x00,
+    0x00,
+    0x00,
+    0x00,
+    0xfe,
+    0x09,
+    0x00,
+    0x12,
+    0x0f,
+    0x01,
+    0x03,
+    0xec,
+    0xc3,
+    0x00,
+    0x10,
+    0xfe,
+    0x40,
+    0x00,
+    0x00,
+    0x5e,
+    0x01,
+    0x68,
+    0x74,
+    0x74,
+    0x70,
+    0x73,
+    0x3a,
+    0x2f,
+    0x2f,
+    0x69,
+    0x6d,
+    0x72,
+    0x69,
+    0x67,
+    0x68,
+    0x74,
+    0x2e,
+    0x6d,
+    0x75,
+    0x64,
+    0x2e,
+    0x65,
+    0x78,
+    0x61,
+    0x6d,
+    0x70,
+    0x6c,
+    0x65,
+    0x2e,
+    0x63,
+    0x6f,
+    0x6d,
+    0x2f,
+    0x2e,
+    0x77,
+    0x65,
+    0x6c,
+    0x6c,
+    0x2d,
+    0x6b,
+    0x6e,
+    0x6f,
+    0x77,
+    0x6e,
+    0x2f,
+    0x6d,
+    0x75,
+    0x64,
+    0x2f,
+    0x76,
+    0x31,
+    0x2f,
+    0x76,
+    0x6f,
+    0x6d,
+    0x69,
+    0x74,
+    0x76,
+    0x32,
+    0x2e,
+
+    0x30,
+    0x00,
+    0x00, /* ethernet trailer */
+);
+
+/*****************************************************************************/
+
+NMTstpSetupFunc const _nmtstp_setup_platform_func = nm_linux_platform_setup;
+
+void
+_nmtstp_init_tests(int *argc, char ***argv)
+{
+    nmtst_init_assert_logging(argc, argv, "WARN", "ALL");
+}
+
+void
+_nmtstp_setup_tests(void)
+{
+#define _TEST_ADD_RECV(testpath, testdata) \
+    g_test_add(testpath,                   \
+               TestRecvFixture,            \
+               testdata,                   \
+               _test_recv_fixture_setup,   \
+               test_recv,                  \
+               _test_recv_fixture_teardown)
+    _TEST_ADD_RECV("/lldp/recv/0", &_test_recv_data0);
+    _TEST_ADD_RECV("/lldp/recv/0_twice", &_test_recv_data0_twice);
+    _TEST_ADD_RECV("/lldp/recv/1", &_test_recv_data1);
+    _TEST_ADD_RECV("/lldp/recv/2_ttl1", &_test_recv_data2_ttl1);
+
+    g_test_add_data_func("/lldp/parse-frames/0", &_test_recv_data0_frame0, test_parse_frames);
+    g_test_add_data_func("/lldp/parse-frames/1", &_test_recv_data1_frame0, test_parse_frames);
+    g_test_add_data_func("/lldp/parse-frames/2", &_test_recv_data2_frame0_ttl1, test_parse_frames);
+    g_test_add_data_func("/lldp/parse-frames/3", &_test_parse_frames_3, test_parse_frames);
+}
diff --git a/src/core/devices/wifi/meson.build b/src/core/devices/wifi/meson.build
new file mode 100644
index 00000000..743937db
--- /dev/null
+++ b/src/core/devices/wifi/meson.build
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+iwd_sources = files()
+if enable_iwd
+  iwd_sources += files(
+    'nm-device-iwd.c',
+    'nm-iwd-manager.c',
+  )
+endif
+
+libnm_device_plugin_wifi_static = static_library(
+  'nm-device-plugin-wifi-static',
+  sources: files(
+    'nm-device-olpc-mesh.c',
+    'nm-device-wifi-p2p.c',
+    'nm-device-wifi.c',
+    'nm-wifi-ap.c',
+    'nm-wifi-common.c',
+    'nm-wifi-p2p-peer.c',
+    'nm-wifi-utils.c',
+  ) + iwd_sources,
+  dependencies: [
+    core_plugin_dep,
+  ],
+  c_args: daemon_c_flags,
+)
+
+libnm_device_plugin_wifi_static_dep = declare_dependency(
+  link_with: libnm_device_plugin_wifi_static,
+)
+
+libnm_device_plugin_wifi = shared_module(
+  'nm-device-plugin-wifi',
+  sources: files(
+    'nm-wifi-factory.c',
+  ),
+  dependencies: [
+    core_plugin_dep,
+    libnm_device_plugin_wifi_static_dep
+  ],
+  c_args: daemon_c_flags,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_wifi
+
+test(
+  'check-local-devices-wifi',
+  check_exports,
+  args: [libnm_device_plugin_wifi.full_path(), linker_script_devices],
+)
+
+if enable_tests
+  test_unit = 'test-devices-wifi'
+
+  exe = executable(
+    test_unit,
+    'tests/' + test_unit + '.c',
+    dependencies: [
+      libNetworkManagerTest_dep,
+      libnm_device_plugin_wifi_static_dep,
+    ],
+    c_args: test_c_flags,
+  )
+
+  test(
+    test_unit,
+    test_script,
+    args: test_args + [exe.full_path()],
+    timeout: default_test_timeout,
+  )
+endif
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
new file mode 100644
index 00000000..f0de90d3
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -0,0 +1,3508 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Intel Corporation
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-iwd.h"
+
+#include <linux/if_ether.h>
+
+#include "devices/nm-device-private.h"
+#include "devices/nm-device.h"
+#include "nm-act-request.h"
+#include "nm-config.h"
+#include "nm-core-internal.h"
+#include "nm-dbus-manager.h"
+#include "nm-glib-aux/nm-ref-string.h"
+#include "nm-iwd-manager.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
+#include "nm-setting-8021x.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-wireless-security.h"
+#include "nm-setting-wireless.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+#include "nm-utils.h"
+#include "nm-wifi-common.h"
+#include "nm-wifi-utils.h"
+#include "settings/nm-settings-connection.h"
+#include "settings/nm-settings.h"
+#include "supplicant/nm-supplicant-types.h"
+#include "nm-auth-utils.h"
+#include "nm-manager.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceIwd
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIwd,
+                             PROP_MODE,
+                             PROP_BITRATE,
+                             PROP_ACCESS_POINTS,
+                             PROP_ACTIVE_ACCESS_POINT,
+                             PROP_CAPABILITIES,
+                             PROP_SCANNING,
+                             PROP_LAST_SCAN, );
+
+typedef struct {
+    GDBusObject *                 dbus_obj;
+    GDBusProxy *                  dbus_device_proxy;
+    GDBusProxy *                  dbus_station_proxy;
+    GDBusProxy *                  dbus_ap_proxy;
+    GDBusProxy *                  dbus_adhoc_proxy;
+    CList                         aps_lst_head;
+    NMWifiAP *                    current_ap;
+    GCancellable *                cancellable;
+    NMDeviceWifiCapabilities      capabilities;
+    NMActRequestGetSecretsCallId *wifi_secrets_id;
+    guint                         periodic_scan_id;
+    guint                         periodic_update_id;
+    bool                          enabled : 1;
+    bool                          can_scan : 1;
+    bool                          nm_autoconnect : 1;
+    bool                          iwd_autoconnect : 1;
+    bool                          scanning : 1;
+    bool                          scan_requested : 1;
+    bool                          act_mode_switch : 1;
+    bool                          secrets_failed : 1;
+    bool                          networks_requested : 1;
+    bool                          networks_changed : 1;
+    gint64                        last_scan;
+    uint32_t                      ap_id;
+    guint32                       rate;
+    NMEtherAddr                   current_ap_bssid;
+    GDBusMethodInvocation *       pending_agent_request;
+    NMActiveConnection *          assumed_ac;
+    guint                         assumed_ac_timeout;
+} NMDeviceIwdPrivate;
+
+struct _NMDeviceIwd {
+    NMDevice           parent;
+    NMDeviceIwdPrivate _priv;
+};
+
+struct _NMDeviceIwdClass {
+    NMDeviceClass parent;
+};
+
+/*****************************************************************************/
+
+G_DEFINE_TYPE(NMDeviceIwd, nm_device_iwd, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_IWD_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceIwd, NM_IS_DEVICE_IWD, NMDevice)
+
+/*****************************************************************************/
+
+static void schedule_periodic_scan(NMDeviceIwd *self, gboolean initial_scan);
+
+static gboolean check_scanning_prohibited(NMDeviceIwd *self, gboolean periodic);
+
+/*****************************************************************************/
+
+static void
+_ap_dump(NMDeviceIwd *self, NMLogLevel log_level, const NMWifiAP *ap, const char *prefix)
+{
+    char buf[1024];
+
+    buf[0] = '\0';
+    _NMLOG(log_level,
+           LOGD_WIFI_SCAN,
+           "wifi-ap: %-7s %s",
+           prefix,
+           nm_wifi_ap_to_string(ap, buf, sizeof(buf), 0));
+}
+
+/* Callers ensure we're not removing current_ap */
+static void
+ap_add_remove(NMDeviceIwd *self,
+              gboolean     is_adding, /* or else is removing */
+              NMWifiAP *   ap,
+              gboolean     recheck_available_connections)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (is_adding) {
+        g_object_ref(ap);
+        ap->wifi_device = NM_DEVICE(self);
+        c_list_link_tail(&priv->aps_lst_head, &ap->aps_lst);
+        nm_dbus_object_export(NM_DBUS_OBJECT(ap));
+        _ap_dump(self, LOGL_DEBUG, ap, "added");
+        nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, TRUE);
+    } else {
+        ap->wifi_device = NULL;
+        c_list_unlink(&ap->aps_lst);
+        _ap_dump(self, LOGL_DEBUG, ap, "removed");
+    }
+
+    _notify(self, PROP_ACCESS_POINTS);
+
+    if (!is_adding) {
+        nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, FALSE);
+        nm_dbus_object_clear_and_unexport(&ap);
+    }
+
+    if (priv->enabled && !priv->iwd_autoconnect)
+        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+
+    if (recheck_available_connections)
+        nm_device_recheck_available_connections(NM_DEVICE(self));
+}
+
+static void
+set_current_ap(NMDeviceIwd *self, NMWifiAP *new_ap, gboolean recheck_available_connections)
+{
+    NMDeviceIwdPrivate *priv;
+    NMWifiAP *          old_ap;
+
+    g_return_if_fail(NM_IS_DEVICE_IWD(self));
+
+    priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    old_ap = priv->current_ap;
+
+    if (old_ap == new_ap)
+        return;
+
+    if (new_ap)
+        priv->current_ap = g_object_ref(new_ap);
+    else
+        priv->current_ap = NULL;
+
+    if (old_ap) {
+        if (nm_wifi_ap_get_fake(old_ap))
+            ap_add_remove(self, FALSE, old_ap, recheck_available_connections);
+        g_object_unref(old_ap);
+    }
+
+    memset(&priv->current_ap_bssid, 0, ETH_ALEN);
+    _notify(self, PROP_ACTIVE_ACCESS_POINT);
+    _notify(self, PROP_MODE);
+}
+
+static void
+remove_all_aps(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMWifiAP *          ap, *ap_safe;
+
+    if (c_list_is_empty(&priv->aps_lst_head))
+        return;
+
+    c_list_for_each_entry_safe (ap, ap_safe, &priv->aps_lst_head, aps_lst)
+        ap_add_remove(self, FALSE, ap, FALSE);
+
+    if (!priv->iwd_autoconnect)
+        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+
+    nm_device_recheck_available_connections(NM_DEVICE(self));
+}
+
+static NM80211ApSecurityFlags
+ap_security_flags_from_network_type(const char *type)
+{
+    NM80211ApSecurityFlags flags;
+
+    if (nm_streq(type, "psk"))
+        flags = NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (nm_streq(type, "8021x"))
+        flags = NM_802_11_AP_SEC_KEY_MGMT_802_1X;
+    else
+        return NM_802_11_AP_SEC_NONE;
+
+    flags |= NM_802_11_AP_SEC_PAIR_CCMP;
+    flags |= NM_802_11_AP_SEC_GROUP_CCMP;
+    return flags;
+}
+
+static NMWifiAP *
+ap_from_network(NMDeviceIwd *self,
+                GDBusProxy * network,
+                NMRefString *bss_path,
+                gint64       last_seen_msec,
+                int16_t      signal)
+{
+    NMDeviceIwdPrivate *priv              = NM_DEVICE_IWD_GET_PRIVATE(self);
+    gs_unref_variant GVariant *name_value = NULL;
+    gs_unref_variant GVariant *type_value = NULL;
+    const char *               name;
+    const char *               type;
+    uint32_t                   ap_id;
+    gs_unref_bytes GBytes *ssid = NULL;
+    NMWifiAP *             ap;
+    NMSupplicantBssInfo    bss_info;
+
+    g_return_val_if_fail(network, NULL);
+
+    name_value = g_dbus_proxy_get_cached_property(network, "Name");
+    type_value = g_dbus_proxy_get_cached_property(network, "Type");
+    if (!name_value || !g_variant_is_of_type(name_value, G_VARIANT_TYPE_STRING) || !type_value
+        || !g_variant_is_of_type(type_value, G_VARIANT_TYPE_STRING))
+        return NULL;
+
+    name = g_variant_get_string(name_value, NULL);
+    type = g_variant_get_string(type_value, NULL);
+
+    if (nm_streq(type, "wep")) {
+        /* WEP not supported */
+        return NULL;
+    }
+
+    /* What we get from IWD are networks, or ESSs, that may contain
+     * multiple APs, or BSSs, each.  We don't get information about any
+     * specific BSSs within an ESS but we can safely present each ESS
+     * as an individual BSS to NM, which will be seen as ESSs comprising
+     * a single BSS each.  NM won't be able to handle roaming but IWD
+     * already does that.  We fake the BSSIDs as they don't play any
+     * role either.
+     */
+    ap_id = priv->ap_id++;
+
+    ssid = g_bytes_new(name, NM_MIN(32u, strlen(name)));
+
+    bss_info = (NMSupplicantBssInfo){
+        .bss_path       = bss_path,
+        .last_seen_msec = last_seen_msec,
+        .bssid_valid    = TRUE,
+        .mode           = NM_802_11_MODE_INFRA,
+        .rsn_flags      = ap_security_flags_from_network_type(type),
+        .ssid           = ssid,
+        .signal_percent = nm_wifi_utils_level_to_quality(signal / 100),
+        .frequency      = 2417,
+        .max_rate       = 65000,
+        .bssid          = NM_ETHER_ADDR_INIT(0x00, 0x01, 0x02, ap_id >> 16, ap_id >> 8, ap_id),
+    };
+
+    ap = nm_wifi_ap_new_from_properties(&bss_info);
+
+    nm_assert(bss_path == nm_wifi_ap_get_supplicant_path(ap));
+
+    return ap;
+}
+
+static void
+insert_ap_from_network(NMDeviceIwd *self,
+                       GHashTable * aps,
+                       const char * path,
+                       gint64       last_seen_msec,
+                       int16_t      signal)
+{
+    gs_unref_object GDBusProxy *network_proxy = NULL;
+    nm_auto_ref_string NMRefString *bss_path  = nm_ref_string_new(path);
+    NMWifiAP *                      ap;
+
+    if (g_hash_table_lookup(aps, bss_path)) {
+        _LOGD(LOGD_WIFI, "Duplicate network at %s", path);
+        return;
+    }
+
+    network_proxy =
+        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), path, NM_IWD_NETWORK_INTERFACE);
+
+    ap = ap_from_network(self, network_proxy, bss_path, last_seen_msec, signal);
+    if (!ap)
+        return;
+
+    g_hash_table_insert(aps, bss_path, ap);
+}
+
+static void
+get_ordered_networks_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *       self = user_data;
+    NMDeviceIwdPrivate *priv;
+    gs_free_error GError *error        = NULL;
+    gs_unref_variant GVariant *variant = NULL;
+    GVariantIter *             networks;
+    const char *               path;
+    int16_t                    signal;
+    NMWifiAP *                 ap, *ap_safe, *new_ap;
+    gboolean                   changed;
+    GHashTableIter             ap_iter;
+    gs_unref_hashtable GHashTable *new_aps = NULL;
+    gint64                         last_seen_msec;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant && nm_utils_error_is_cancelled(error))
+        return;
+
+    priv                     = NM_DEVICE_IWD_GET_PRIVATE(self);
+    priv->networks_requested = FALSE;
+
+    if (!variant) {
+        _LOGE(LOGD_WIFI, "Station.GetOrderedNetworks failed: %s", error->message);
+        return;
+    }
+
+    if (!g_variant_is_of_type(variant, G_VARIANT_TYPE("(a(on))"))) {
+        _LOGE(LOGD_WIFI,
+              "Station.GetOrderedNetworks returned type %s instead of (a(on))",
+              g_variant_get_type_string(variant));
+        return;
+    }
+
+    new_aps = g_hash_table_new_full(nm_direct_hash, NULL, NULL, g_object_unref);
+    g_variant_get(variant, "(a(on))", &networks);
+
+    last_seen_msec = nm_utils_get_monotonic_timestamp_msec();
+    while (g_variant_iter_next(networks, "(&on)", &path, &signal))
+        insert_ap_from_network(self, new_aps, path, last_seen_msec, signal);
+
+    g_variant_iter_free(networks);
+
+    changed                = priv->networks_changed;
+    priv->networks_changed = FALSE;
+
+    c_list_for_each_entry_safe (ap, ap_safe, &priv->aps_lst_head, aps_lst) {
+        new_ap = g_hash_table_lookup(new_aps, nm_wifi_ap_get_supplicant_path(ap));
+        if (new_ap) {
+            if (nm_wifi_ap_set_strength(ap, nm_wifi_ap_get_strength(new_ap))) {
+                _ap_dump(self, LOGL_TRACE, ap, "updated");
+                changed = TRUE;
+            }
+            g_hash_table_remove(new_aps, nm_wifi_ap_get_supplicant_path(ap));
+            continue;
+        }
+
+        if (ap == priv->current_ap) {
+            /* Normally IWD will prevent the current AP from being
+             * removed from the list and set a low signal strength,
+             * but just making sure.
+             */
+            continue;
+        }
+
+        ap_add_remove(self, FALSE, ap, FALSE);
+        changed = TRUE;
+    }
+
+    g_hash_table_iter_init(&ap_iter, new_aps);
+    while (g_hash_table_iter_next(&ap_iter, NULL, (gpointer) &ap)) {
+        ap_add_remove(self, TRUE, ap, FALSE);
+        g_hash_table_iter_remove(&ap_iter);
+        changed = TRUE;
+    }
+
+    if (changed) {
+        if (!priv->iwd_autoconnect)
+            nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+
+        nm_device_recheck_available_connections(NM_DEVICE(self));
+    }
+}
+
+static void
+update_aps(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (!priv->cancellable)
+        priv->cancellable = g_cancellable_new();
+
+    g_dbus_proxy_call(priv->dbus_station_proxy,
+                      "GetOrderedNetworks",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      2000,
+                      priv->cancellable,
+                      get_ordered_networks_cb,
+                      self);
+    priv->networks_requested = TRUE;
+}
+
+static void
+periodic_update(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    int                 ifindex;
+    guint32             new_rate;
+    int                 percent;
+    NMEtherAddr         bssid;
+    gboolean            ap_changed = FALSE;
+    NMPlatform *        platform;
+
+    ifindex = nm_device_get_ifindex(NM_DEVICE(self));
+    if (ifindex <= 0)
+        g_return_if_reached();
+
+    platform = nm_device_get_platform(NM_DEVICE(self));
+
+    /* TODO: obtain quality through the net.connman.iwd.SignalLevelAgent API.
+     * For now we're waking up for the rate/BSSID updates anyway.
+     */
+    if (!nm_platform_wifi_get_station(platform, ifindex, &bssid, &percent, &new_rate)) {
+        _LOGD(LOGD_WIFI, "BSSID / quality / rate platform query failed");
+        return;
+    }
+
+    if (nm_wifi_ap_set_strength(priv->current_ap, (gint8) percent)) {
+#if NM_MORE_LOGGING
+        ap_changed = TRUE;
+#endif
+    }
+
+    if (new_rate != priv->rate) {
+        priv->rate = new_rate;
+        _notify(self, PROP_BITRATE);
+    }
+
+    if (nm_ether_addr_is_valid(&bssid) && !nm_ether_addr_equal(&bssid, &priv->current_ap_bssid)) {
+        priv->current_ap_bssid = bssid;
+        ap_changed |= nm_wifi_ap_set_address_bin(priv->current_ap, &bssid);
+        ap_changed |= nm_wifi_ap_set_freq(priv->current_ap,
+                                          nm_platform_wifi_get_frequency(platform, ifindex));
+    }
+
+    if (ap_changed)
+        _ap_dump(self, LOGL_DEBUG, priv->current_ap, "updated");
+}
+
+static gboolean
+periodic_update_cb(gpointer user_data)
+{
+    periodic_update(user_data);
+    return TRUE;
+}
+
+static void
+send_disconnect(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    g_dbus_proxy_call(priv->dbus_station_proxy,
+                      "Disconnect",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      -1,
+                      NULL,
+                      NULL,
+                      NULL);
+}
+
+static void
+wifi_secrets_cancel(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (priv->wifi_secrets_id)
+        nm_act_request_cancel_secrets(NULL, priv->wifi_secrets_id);
+    nm_assert(!priv->wifi_secrets_id);
+
+    if (priv->pending_agent_request) {
+        g_dbus_method_invocation_return_error_literal(priv->pending_agent_request,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                      "NM secrets request cancelled");
+        g_clear_object(&priv->pending_agent_request);
+    }
+}
+
+static void
+cleanup_assumed_connect(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (!priv->assumed_ac)
+        return;
+
+    g_signal_handlers_disconnect_by_data(priv->assumed_ac, self);
+    g_clear_object(&priv->assumed_ac);
+}
+
+static void
+cleanup_association_attempt(NMDeviceIwd *self, gboolean disconnect)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    cleanup_assumed_connect(self);
+    wifi_secrets_cancel(self);
+
+    set_current_ap(self, NULL, TRUE);
+    nm_clear_g_source(&priv->periodic_update_id);
+    nm_clear_g_source(&priv->assumed_ac_timeout);
+
+    if (disconnect && priv->dbus_station_proxy)
+        send_disconnect(self);
+}
+
+static void
+reset_mode(NMDeviceIwd *       self,
+           GCancellable *      cancellable,
+           GAsyncReadyCallback callback,
+           gpointer            user_data)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    g_dbus_proxy_call(
+        priv->dbus_device_proxy,
+        DBUS_INTERFACE_PROPERTIES ".Set",
+        g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Mode", g_variant_new_string("station")),
+        G_DBUS_CALL_FLAGS_NONE,
+        2000,
+        cancellable,
+        callback,
+        user_data);
+}
+
+static gboolean
+get_variant_boolean(GVariant *v, const char *property)
+{
+    if (!v || !g_variant_is_of_type(v, G_VARIANT_TYPE_BOOLEAN)) {
+        nm_log_warn(LOGD_DEVICE | LOGD_WIFI,
+                    "Property %s not cached or not boolean type",
+                    property);
+
+        return FALSE;
+    }
+
+    return g_variant_get_boolean(v);
+}
+
+static const char *
+get_variant_state(GVariant *v)
+{
+    if (!v || !g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) {
+        nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "State property not cached or not a string");
+
+        return "unknown";
+    }
+
+    return g_variant_get_string(v, NULL);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (!priv->dbus_obj)
+        return;
+
+    if (priv->dbus_station_proxy) {
+        gs_unref_variant GVariant *value =
+            g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+
+        if (NM_IN_STRSET(get_variant_state(value), "disconnecting", "disconnected"))
+            return;
+    }
+
+    cleanup_association_attempt(self, TRUE);
+    priv->act_mode_switch = FALSE;
+
+    if (!priv->dbus_station_proxy)
+        reset_mode(self, NULL, NULL, NULL);
+}
+
+static void
+disconnect_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_object NMDeviceIwd *self = NULL;
+    NMDeviceDeactivateCallback   callback;
+    gpointer                     callback_user_data;
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    callback(NM_DEVICE(self), error, callback_user_data);
+}
+
+static void
+disconnect_cb_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    gs_unref_object NMDeviceIwd *self = NULL;
+    NMDeviceDeactivateCallback   callback;
+    gpointer                     callback_user_data;
+    gs_free_error GError *cancelled_error = NULL;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+
+    g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error);
+    callback(NM_DEVICE(self), cancelled_error, callback_user_data);
+}
+
+static void
+deactivate_async(NMDevice *                 device,
+                 GCancellable *             cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   callback_user_data)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    gpointer            user_data;
+
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+
+    user_data = nm_utils_user_data_pack(g_object_ref(self), callback, callback_user_data);
+
+    if (!priv->dbus_obj) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_cb_on_idle, user_data);
+        return;
+    }
+
+    cleanup_association_attempt(self, FALSE);
+    priv->act_mode_switch = FALSE;
+
+    if (priv->dbus_station_proxy) {
+        g_dbus_proxy_call(priv->dbus_station_proxy,
+                          "Disconnect",
+                          NULL,
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          cancellable,
+                          disconnect_cb,
+                          user_data);
+    } else
+        reset_mode(self, cancellable, disconnect_cb, user_data);
+}
+
+static gboolean
+is_connection_known_network(NMConnection *connection)
+{
+    NMIwdNetworkSecurity security;
+    gs_free char *       ssid = NULL;
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, &security))
+        return FALSE;
+
+    return nm_iwd_manager_is_known_network(nm_iwd_manager_get(), ssid, security);
+}
+
+static gboolean
+is_ap_known_network(NMWifiAP *ap)
+{
+    gs_unref_object GDBusProxy *network_proxy = NULL;
+    gs_unref_variant GVariant *known_network  = NULL;
+
+    network_proxy =
+        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+                                          nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
+                                          NM_IWD_NETWORK_INTERFACE);
+    if (!network_proxy)
+        return FALSE;
+
+    known_network = g_dbus_proxy_get_cached_property(network_proxy, "KnownNetwork");
+    return nm_g_variant_is_of_type(known_network, G_VARIANT_TYPE_OBJECT_PATH);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceIwd *        self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMSettingWireless *  s_wireless;
+    const char *         mac;
+    const char *const *  mac_blacklist;
+    int                  i;
+    const char *         perm_hw_addr;
+    const char *         mode;
+    NMIwdNetworkSecurity security;
+    GBytes *             ssid;
+    const guint8 *       ssid_bytes;
+    gsize                ssid_len;
+
+    if (!NM_DEVICE_CLASS(nm_device_iwd_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+
+    /* complete_connection would be called (if at all) before this function
+     * so an SSID should always be set.  IWD doesn't support non-UTF8 SSIDs
+     * (ignores BSSes with such SSIDs and has no way to represent them on
+     * DBus) so we can cut it short for connections with a non-UTF8 SSID.
+     */
+    ssid = nm_setting_wireless_get_ssid(s_wireless);
+    if (!ssid)
+        return FALSE;
+
+    ssid_bytes = g_bytes_get_data(ssid, &ssid_len);
+    if (!g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                   "non-UTF-8 connection SSID not supported by IWD backend");
+        return FALSE;
+    }
+
+    perm_hw_addr = nm_device_get_permanent_hw_address(device);
+    mac          = nm_setting_wireless_get_mac_address(s_wireless);
+    if (perm_hw_addr) {
+        if (mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "device MAC address does not match the profile");
+            return FALSE;
+        }
+
+        /* Check for MAC address blacklist */
+        mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless);
+        for (i = 0; mac_blacklist[i]; i++) {
+            nm_assert(nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN));
+
+            if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "MAC address blacklisted");
+                return FALSE;
+            }
+        }
+    } else if (mac) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device has no valid MAC address as required by profile");
+        return FALSE;
+    }
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security)
+        || security == NM_IWD_NETWORK_SECURITY_WEP) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                   "connection authentication type not supported by IWD backend");
+        return FALSE;
+    }
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+
+    /* Hidden SSIDs only supported in client mode */
+    if (nm_setting_wireless_get_hidden(s_wireless)
+        && !NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) {
+        nm_utils_error_set_literal(
+            error,
+            NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+            "non-infrastructure hidden networks not supported by the IWD backend");
+        return FALSE;
+    }
+
+    if (NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) {
+        /* 8021x networks can only be used if they've been provisioned on the IWD side and
+         * thus are Known Networks.
+         */
+        if (security == NM_IWD_NETWORK_SECURITY_8021X) {
+            if (!is_connection_known_network(connection)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                           "802.1x connections must have IWD provisioning files");
+                return FALSE;
+            }
+        } else if (!NM_IN_SET(security,
+                              NM_IWD_NETWORK_SECURITY_OPEN,
+                              NM_IWD_NETWORK_SECURITY_PSK)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "IWD backend only supports Open, PSK and 802.1x network "
+                                       "authentication in Infrastructure mode");
+            return FALSE;
+        }
+    } else if (nm_streq(mode, NM_SETTING_WIRELESS_MODE_AP)) {
+        NMSettingWirelessSecurity *s_wireless_sec =
+            nm_connection_get_setting_wireless_security(connection);
+
+        if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_AP)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "device does not support Access Point mode");
+            return FALSE;
+        }
+
+        if (!NM_IN_SET(security, NM_IWD_NETWORK_SECURITY_PSK) || !s_wireless_sec
+            || !nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec), "wpa-psk")) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "IWD backend only supports PSK authentication in AP mode");
+            return FALSE;
+        }
+    } else if (nm_streq(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) {
+        NMSettingWirelessSecurity *s_wireless_sec =
+            nm_connection_get_setting_wireless_security(connection);
+
+        if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_ADHOC)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                                       "device does not support Ad-Hoc mode");
+            return FALSE;
+        }
+
+        if (!NM_IN_SET(security, NM_IWD_NETWORK_SECURITY_OPEN, NM_IWD_NETWORK_SECURITY_PSK)
+            || (s_wireless_sec
+                && !nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec),
+                              "wpa-psk"))) {
+            nm_utils_error_set_literal(
+                error,
+                NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                "IWD backend only supports Open and PSK authentication in Ad-Hoc mode");
+            return FALSE;
+        }
+    } else {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                           "'%s' type profiles not supported by IWD backend",
+                           mode);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceIwd *        self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate * priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMSettingWireless *  s_wifi;
+    const char *         mode;
+    NMWifiAP *           ap = NULL;
+    NMIwdNetworkSecurity security;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wifi, FALSE);
+
+    /* a connection that is available for a certain @specific_object, MUST
+     * also be available in general (without @specific_object). */
+
+    if (specific_object) {
+        ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object);
+        if (!ap) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "requested access point not found");
+            return FALSE;
+        }
+        if (!nm_wifi_ap_check_compatible(ap, connection)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "requested access point is not compatible with profile");
+            return FALSE;
+        }
+    }
+
+    /* AP and Ad-Hoc connections can be activated independent of the scan list */
+    mode = nm_setting_wireless_get_mode(s_wifi);
+    if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC))
+        return TRUE;
+
+    /* Hidden SSIDs obviously don't always appear in the scan list either.
+     *
+     * For an explicit user-activation-request, a connection is considered
+     * available because for hidden Wi-Fi, clients didn't consistently
+     * set the 'hidden' property to indicate hidden SSID networks.  If
+     * activating but the network isn't available let the device recheck
+     * availability.
+     */
+    if (nm_setting_wireless_get_hidden(s_wifi)
+        || NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP))
+        return TRUE;
+
+    if (!ap)
+        ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+
+    if (!ap) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "no compatible access point found");
+        return FALSE;
+    }
+
+    /* 8021x networks can only be used if they've been provisioned on the IWD side and
+     * thus are Known Networks.
+     */
+    if (nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security)
+        && security == NM_IWD_NETWORK_SECURITY_8021X) {
+        if (!is_ap_known_network(ap)) {
+            nm_utils_error_set_literal(
+                error,
+                NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                "802.1x network is not an IWD Known Network (missing provisioning file?)");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+/* To be used where the SSID has been validated before */
+static char *
+iwd_ssid_to_str(const GBytes *ssid)
+{
+    const guint8 *ssid_bytes;
+    gsize         ssid_len;
+
+    ssid_bytes = g_bytes_get_data((GBytes *) ssid, &ssid_len);
+    nm_assert(ssid && g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL));
+    return g_strndup((const char *) ssid_bytes, ssid_len);
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMSettingWireless * s_wifi;
+    gs_free char *      ssid_utf8 = NULL;
+    NMWifiAP *          ap;
+    GBytes *            ssid   = NULL;
+    gboolean            hidden = FALSE;
+    const char *        mode;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+
+    mode = s_wifi ? nm_setting_wireless_get_mode(s_wifi) : NULL;
+
+    if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP) || !specific_object) {
+        const guint8 *ssid_bytes;
+        gsize         ssid_len;
+
+        /* If not given a specific object, we need at minimum an SSID */
+        if (!s_wifi) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                "A 'wireless' setting is required if no AP path was given.");
+            return FALSE;
+        }
+
+        ssid       = nm_setting_wireless_get_ssid(s_wifi);
+        ssid_bytes = g_bytes_get_data(ssid, &ssid_len);
+
+        if (!ssid || ssid_len == 0 || !g_utf8_validate((const char *) ssid_bytes, ssid_len, NULL)) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                "A 'wireless' setting with a valid UTF-8 SSID is required if no AP "
+                                "path was given.");
+            return FALSE;
+        }
+    }
+
+    if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) {
+        if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error))
+            return FALSE;
+        ap = NULL;
+    } else if (!specific_object) {
+        /* Find a compatible AP in the scan list */
+        ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+        if (!ap) {
+            /* If we still don't have an AP, then the WiFI settings needs to be
+             * fully specified by the client.  Might not be able to find an AP
+             * if the network isn't broadcasting the SSID for example.
+             */
+            if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error))
+                return FALSE;
+
+            /* We could either require the profile to be marked as hidden by the
+             * client or at least check that a hidden AP with a matching security
+             * type is in range using Station.GetHiddenAccessPoints().  For now
+             * assume it is hidden even though that will reveal the SSID on the
+             * air.
+             */
+            hidden = TRUE;
+        }
+    } else {
+        ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object);
+        if (!ap) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND,
+                        "The access point %s was not in the scan list.",
+                        specific_object);
+            return FALSE;
+        }
+
+        ssid = nm_wifi_ap_get_ssid(ap);
+
+        /* Add a wifi setting if one doesn't exist yet */
+        if (!s_wifi) {
+            s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_wifi));
+        }
+    }
+
+    if (ap) {
+        if (!nm_wifi_ap_complete_connection(ap, connection, FALSE, error))
+            return FALSE;
+    }
+
+    ssid_utf8 = iwd_ssid_to_str(ssid);
+    nm_utils_complete_generic(
+        nm_device_get_platform(device),
+        connection,
+        NM_SETTING_WIRELESS_SETTING_NAME,
+        existing_connections,
+        ssid_utf8,
+        ssid_utf8,
+        NULL,
+        nm_setting_wireless_get_mac_address(s_wifi) ? NULL : nm_device_get_iface(device),
+        TRUE);
+
+    if (hidden)
+        g_object_set(s_wifi, NM_SETTING_WIRELESS_HIDDEN, TRUE, NULL);
+
+    return TRUE;
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceIwd *       self  = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv  = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDeviceState       state = nm_device_get_state(device);
+
+    /* Available if either the device is UP and in station mode
+     * or in AP/Ad-Hoc modes while activating or activated.  Device
+     * may be temporarily DOWN while activating or deactivating and
+     * we don't want it to be marked unavailable because of this.
+     *
+     * For reference:
+     * We call nm_device_queue_recheck_available whenever
+     * priv->enabled changes or priv->dbus_station_proxy changes.
+     */
+    return priv->dbus_obj && priv->enabled
+           && (priv->dbus_station_proxy
+               || (state >= NM_DEVICE_STATE_CONFIG && state <= NM_DEVICE_STATE_DEACTIVATING));
+}
+
+static gboolean
+get_autoconnect_allowed(NMDevice *device)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(NM_DEVICE_IWD(device));
+
+    return priv->nm_autoconnect;
+}
+
+static gboolean
+can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMConnection *      connection;
+    NMSettingWireless * s_wifi;
+    NMWifiAP *          ap;
+    const char *        mode;
+    guint64             timestamp = 0;
+
+    nm_assert(!specific_object || !*specific_object);
+
+    if (!NM_DEVICE_CLASS(nm_device_iwd_parent_class)->can_auto_connect(device, sett_conn, NULL))
+        return FALSE;
+
+    connection = nm_settings_connection_get_connection(sett_conn);
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wifi, FALSE);
+
+    /* Don't auto-activate AP or Ad-Hoc connections.
+     * Note the wpa_supplicant backend has the opposite policy.
+     */
+    mode = nm_setting_wireless_get_mode(s_wifi);
+    if (mode && g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_INFRA) != 0)
+        return FALSE;
+
+    /* Don't autoconnect to networks that have been tried at least once
+     * but haven't been successful, since these are often accidental choices
+     * from the menu and the user may not know the password.
+     */
+    if (nm_settings_connection_get_timestamp(sett_conn, &timestamp)) {
+        if (timestamp == 0)
+            return FALSE;
+    }
+
+    ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+    if (ap) {
+        /* All good; connection is usable */
+        NM_SET_OUT(specific_object, g_strdup(nm_dbus_object_get_path(NM_DBUS_OBJECT(ap))));
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+const CList *
+_nm_device_iwd_get_aps(NMDeviceIwd *self)
+{
+    return &NM_DEVICE_IWD_GET_PRIVATE(self)->aps_lst_head;
+}
+
+static void
+scan_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *       self = user_data;
+    NMDeviceIwdPrivate *priv;
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant && nm_utils_error_is_cancelled(error))
+        return;
+
+    priv                 = NM_DEVICE_IWD_GET_PRIVATE(self);
+    priv->scan_requested = FALSE;
+    priv->last_scan      = nm_utils_get_monotonic_timestamp_msec();
+    _notify(self, PROP_LAST_SCAN);
+
+    /* On success, priv->scanning becomes true right before or right
+     * after this callback, so the next automatic scan will be
+     * scheduled when priv->scanning goes back to false.  On error,
+     * schedule a retry now.
+     */
+    if (error && !priv->scanning)
+        schedule_periodic_scan(self, FALSE);
+}
+
+static void
+dbus_request_scan_cb(NMDevice *             device,
+                     GDBusMethodInvocation *context,
+                     NMAuthSubject *        subject,
+                     GError *               error,
+                     gpointer               user_data)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv;
+    gs_unref_variant GVariant *scan_options = user_data;
+
+    if (error) {
+        g_dbus_method_invocation_return_gerror(context, error);
+        return;
+    }
+
+    if (check_scanning_prohibited(self, FALSE)) {
+        g_dbus_method_invocation_return_error_literal(context,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ALLOWED,
+                                                      "Scanning not allowed at this time");
+        return;
+    }
+
+    priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (!priv->can_scan) {
+        g_dbus_method_invocation_return_error_literal(context,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ALLOWED,
+                                                      "Scanning not allowed while unavailable");
+        return;
+    }
+
+    if (scan_options) {
+        gs_unref_variant GVariant *val = g_variant_lookup_value(scan_options, "ssids", NULL);
+
+        if (val) {
+            g_dbus_method_invocation_return_error_literal(context,
+                                                          NM_DEVICE_ERROR,
+                                                          NM_DEVICE_ERROR_NOT_ALLOWED,
+                                                          "'ssid' scan option not supported");
+            return;
+        }
+    }
+
+    if (!priv->scanning && !priv->scan_requested) {
+        g_dbus_proxy_call(priv->dbus_station_proxy,
+                          "Scan",
+                          NULL,
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          priv->cancellable,
+                          scan_cb,
+                          self);
+        priv->scan_requested = TRUE;
+    }
+
+    g_dbus_method_invocation_return_value(context, NULL);
+}
+
+void
+_nm_device_iwd_request_scan(NMDeviceIwd *self, GVariant *options, GDBusMethodInvocation *invocation)
+{
+    NMDeviceIwdPrivate *priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device = NM_DEVICE(self);
+
+    if (!priv->can_scan) {
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ALLOWED,
+                                                      "Scanning not allowed while unavailable");
+        return;
+    }
+
+    nm_device_auth_request(device,
+                           invocation,
+                           NULL,
+                           NM_AUTH_PERMISSION_WIFI_SCAN,
+                           TRUE,
+                           NULL,
+                           dbus_request_scan_cb,
+                           nm_g_variant_ref(options));
+}
+
+static gboolean
+check_scanning_prohibited(NMDeviceIwd *self, gboolean periodic)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->dbus_obj != NULL, TRUE);
+
+    switch (nm_device_get_state(NM_DEVICE(self))) {
+    case NM_DEVICE_STATE_UNKNOWN:
+    case NM_DEVICE_STATE_UNMANAGED:
+    case NM_DEVICE_STATE_UNAVAILABLE:
+    case NM_DEVICE_STATE_PREPARE:
+    case NM_DEVICE_STATE_CONFIG:
+    case NM_DEVICE_STATE_IP_CONFIG:
+    case NM_DEVICE_STATE_IP_CHECK:
+    case NM_DEVICE_STATE_SECONDARIES:
+    case NM_DEVICE_STATE_DEACTIVATING:
+        /* Prohibit scans when unusable or activating */
+        return TRUE;
+    case NM_DEVICE_STATE_DISCONNECTED:
+    case NM_DEVICE_STATE_FAILED:
+    case NM_DEVICE_STATE_ACTIVATED:
+    case NM_DEVICE_STATE_NEED_AUTH:
+        break;
+    }
+
+    /* Prohibit scans if IWD is busy */
+    return !priv->can_scan;
+}
+
+static const char *
+get_agent_request_network_path(GDBusMethodInvocation *invocation)
+{
+    const char *method_name  = g_dbus_method_invocation_get_method_name(invocation);
+    GVariant *  params       = g_dbus_method_invocation_get_parameters(invocation);
+    const char *network_path = NULL;
+
+    if (nm_streq(method_name, "RequestPassphrase"))
+        g_variant_get(params, "(o)", &network_path);
+    else if (nm_streq(method_name, "RequestPrivateKeyPassphrase"))
+        g_variant_get(params, "(o)", &network_path);
+    else if (nm_streq(method_name, "RequestUserNameAndPassword"))
+        g_variant_get(params, "(o)", &network_path);
+    else if (nm_streq(method_name, "RequestUserPassword")) {
+        const char *user;
+        g_variant_get(params, "(os)", &network_path, &user);
+    }
+
+    return network_path;
+}
+
+/*
+ * try_reply_agent_request
+ *
+ * Check if the connection settings already have the secrets corresponding
+ * to the IWD agent method that was invoked.  If they do, send the method reply
+ * with the appropriate secrets.  Otherwise, return the missing secret's setting
+ * name and key so the caller can send a NM secrets request with this data.
+ * Return TRUE in either case, return FALSE if an error is detected.
+ */
+static gboolean
+try_reply_agent_request(NMDeviceIwd *          self,
+                        NMConnection *         connection,
+                        GDBusMethodInvocation *invocation,
+                        const char **          setting_name,
+                        const char **          setting_key,
+                        gboolean *             replied)
+{
+    const char *               method_name = g_dbus_method_invocation_get_method_name(invocation);
+    NMSettingWirelessSecurity *s_wireless_sec;
+    NMSetting8021x *           s_8021x;
+
+    s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+    s_8021x        = nm_connection_get_setting_802_1x(connection);
+
+    *replied = FALSE;
+
+    if (nm_streq(method_name, "RequestPassphrase")) {
+        const char *psk;
+
+        if (!s_wireless_sec)
+            return FALSE;
+
+        psk = nm_setting_wireless_security_get_psk(s_wireless_sec);
+        if (psk) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the PSK to the IWD Agent");
+
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", psk));
+            *replied = TRUE;
+            return TRUE;
+        }
+
+        *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+        *setting_key  = NM_SETTING_WIRELESS_SECURITY_PSK;
+        return TRUE;
+    } else if (nm_streq(method_name, "RequestPrivateKeyPassphrase")) {
+        const char *password;
+
+        if (!s_8021x)
+            return FALSE;
+
+        password = nm_setting_802_1x_get_private_key_password(s_8021x);
+        if (password) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the private key password to the IWD Agent");
+
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", password));
+            *replied = TRUE;
+            return TRUE;
+        }
+
+        *setting_name = NM_SETTING_802_1X_SETTING_NAME;
+        *setting_key  = NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD;
+        return TRUE;
+    } else if (nm_streq(method_name, "RequestUserNameAndPassword")) {
+        const char *identity, *password;
+
+        if (!s_8021x)
+            return FALSE;
+
+        identity = nm_setting_802_1x_get_identity(s_8021x);
+        password = nm_setting_802_1x_get_password(s_8021x);
+        if (identity && password) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the username and password to the IWD Agent");
+
+            g_dbus_method_invocation_return_value(invocation,
+                                                  g_variant_new("(ss)", identity, password));
+            *replied = TRUE;
+            return TRUE;
+        }
+
+        *setting_name = NM_SETTING_802_1X_SETTING_NAME;
+        if (!identity)
+            *setting_key = NM_SETTING_802_1X_IDENTITY;
+        else
+            *setting_key = NM_SETTING_802_1X_PASSWORD;
+        return TRUE;
+    } else if (nm_streq(method_name, "RequestUserPassword")) {
+        const char *password;
+
+        if (!s_8021x)
+            return FALSE;
+
+        password = nm_setting_802_1x_get_password(s_8021x);
+        if (password) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Returning the user password to the IWD Agent");
+
+            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", password));
+            *replied = TRUE;
+            return TRUE;
+        }
+
+        *setting_name = NM_SETTING_802_1X_SETTING_NAME;
+        *setting_key  = NM_SETTING_802_1X_PASSWORD;
+        return TRUE;
+    } else
+        return FALSE;
+}
+
+static gboolean
+assumed_ac_timeout_cb(gpointer user_data)
+{
+    NMDeviceIwd *       self = user_data;
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    nm_assert(priv->assumed_ac);
+
+    priv->assumed_ac_timeout = 0;
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_FAILED,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
+    /* NMDevice's state change -> NMActRequests/NMActiveConnection's state
+     * change -> assumed_connection_state_changed_before_managed() ->
+     * cleanup_association_attempt() so no need to call it explicitly.
+     */
+    return G_SOURCE_REMOVE;
+}
+
+static void wifi_secrets_get_one(NMDeviceIwd *                self,
+                                 const char *                 setting_name,
+                                 NMSecretAgentGetSecretsFlags flags,
+                                 const char *                 setting_key,
+                                 GDBusMethodInvocation *      invocation);
+
+static void
+wifi_secrets_cb(NMActRequest *                req,
+                NMActRequestGetSecretsCallId *call_id,
+                NMSettingsConnection *        s_connection,
+                GError *                      error,
+                gpointer                      user_data)
+{
+    NMDeviceIwd *                self;
+    NMDeviceIwdPrivate *         priv;
+    NMDevice *                   device;
+    GDBusMethodInvocation *      invocation;
+    const char *                 setting_name;
+    const char *                 setting_key;
+    gboolean                     replied;
+    NMSecretAgentGetSecretsFlags get_secret_flags =
+        NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+
+    nm_utils_user_data_unpack(user_data, &self, &invocation);
+
+    g_return_if_fail(NM_IS_DEVICE_IWD(self));
+
+    priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    device = NM_DEVICE(self);
+
+    g_return_if_fail(priv->wifi_secrets_id == call_id);
+
+    priv->wifi_secrets_id = NULL;
+
+    if (nm_utils_error_is_cancelled(error)) {
+        priv->secrets_failed = TRUE;
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                      "NM secrets request cancelled");
+        return;
+    }
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == s_connection);
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_NEED_AUTH)
+        goto secrets_error;
+
+    if (error) {
+        _LOGW(LOGD_WIFI, "%s", error->message);
+        goto secrets_error;
+    }
+
+    if (!try_reply_agent_request(self,
+                                 nm_act_request_get_applied_connection(req),
+                                 invocation,
+                                 &setting_name,
+                                 &setting_key,
+                                 &replied))
+        goto secrets_error;
+
+    if (replied) {
+        /* If we replied to the secrets request from IWD in the "disconnected"
+         * state and IWD doesn't move to a new state within 1 second, assume
+         * something went wrong (shouldn't happen).  If a state change arrives
+         * after that nothing is lost, state_changed() will try to assume the
+         * connection again.
+         */
+        if (priv->assumed_ac) {
+            gs_unref_variant GVariant *value =
+                g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+
+            if (nm_streq(get_variant_state(value), "disconnected"))
+                priv->assumed_ac_timeout = g_timeout_add_seconds(1, assumed_ac_timeout_cb, self);
+        }
+
+        /* Change state back to what it was before NEED_AUTH */
+        nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+        return;
+    }
+
+    if (nm_settings_connection_get_timestamp(nm_act_request_get_settings_connection(req), NULL))
+        get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+
+    /* Request further secrets if we still need something */
+    wifi_secrets_get_one(self, setting_name, get_secret_flags, setting_key, invocation);
+    return;
+
+secrets_error:
+    g_dbus_method_invocation_return_error_literal(invocation,
+                                                  NM_DEVICE_ERROR,
+                                                  NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                  "NM secrets request failed");
+
+    if (priv->assumed_ac) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        /* NMDevice's state change -> NMActRequests/NMActiveConnection's state
+         * change -> assumed_connection_state_changed_before_managed() ->
+         * cleanup_association_attempt() so no need to call it explicitly.
+         */
+    } else {
+        priv->secrets_failed = TRUE;
+        /* Now wait for the Connect callback to update device state */
+    }
+}
+
+static void
+wifi_secrets_get_one(NMDeviceIwd *                self,
+                     const char *                 setting_name,
+                     NMSecretAgentGetSecretsFlags flags,
+                     const char *                 setting_key,
+                     GDBusMethodInvocation *      invocation)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMActRequest *      req;
+
+    wifi_secrets_cancel(self);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv->wifi_secrets_id = nm_act_request_get_secrets(req,
+                                                       TRUE,
+                                                       setting_name,
+                                                       flags,
+                                                       NM_MAKE_STRV(setting_key),
+                                                       wifi_secrets_cb,
+                                                       nm_utils_user_data_pack(self, invocation));
+}
+
+static void
+network_connect_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *       self           = user_data;
+    NMDevice *          device         = NM_DEVICE(self);
+    NMDeviceIwdPrivate *priv           = NM_DEVICE_IWD_GET_PRIVATE(self);
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+    NMConnection *        connection;
+    gs_free char *        ssid   = NULL;
+    NMDeviceStateReason   reason = NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED;
+    GVariant *            value;
+    gboolean              disconnect;
+
+    disconnect = !priv->iwd_autoconnect
+                 || nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL);
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant) {
+        gs_free char *dbus_error = NULL;
+
+        /* Connection failed; radio problems or if the network wasn't
+         * open, the passwords or certificates may be wrong.
+         */
+
+        _LOGE(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) Network.Connect failed: %s",
+              error->message);
+
+        if (nm_utils_error_is_cancelled(error))
+            return;
+
+        if (!NM_IN_SET(nm_device_get_state(device),
+                       NM_DEVICE_STATE_CONFIG,
+                       NM_DEVICE_STATE_NEED_AUTH))
+            return;
+
+        connection = nm_device_get_applied_connection(device);
+        if (!connection)
+            goto failed;
+
+        if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR))
+            dbus_error = g_dbus_error_get_remote_error(error);
+
+        if (nm_streq0(dbus_error, "net.connman.iwd.Failed")) {
+            nm_connection_clear_secrets(connection);
+
+            /* If secrets were wrong, we'd be getting a net.connman.iwd.Failed */
+            reason = NM_DEVICE_STATE_REASON_NO_SECRETS;
+        } else if (nm_streq0(dbus_error, "net.connman.iwd.Aborted") && priv->secrets_failed) {
+            /* If agent call was cancelled we'd be getting a net.connman.iwd.Aborted */
+            reason = NM_DEVICE_STATE_REASON_NO_SECRETS;
+        }
+
+        goto failed;
+    }
+
+    nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG);
+
+    disconnect = TRUE;
+
+    connection = nm_device_get_applied_connection(device);
+    if (!connection)
+        goto failed;
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, NULL))
+        goto failed;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) Stage 2 of 5 (Device Configure) successful.  Connected to '%s'.",
+          ssid);
+    nm_device_activate_schedule_stage3_ip_config_start(device);
+
+    return;
+
+failed:
+    /* If necessary call Disconnect to make sure IWD's autoconnect is disabled */
+    cleanup_association_attempt(self, disconnect);
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+
+    value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+    if (!priv->iwd_autoconnect && nm_streq(get_variant_state(value), "disconnected")) {
+        schedule_periodic_scan(self, TRUE);
+
+        if (!priv->nm_autoconnect) {
+            priv->nm_autoconnect = true;
+            nm_device_emit_recheck_auto_activate(device);
+        }
+    }
+    g_variant_unref(value);
+}
+
+static void
+act_failed_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *    self              = user_data;
+    NMDevice *       device            = NM_DEVICE(self);
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant && nm_utils_error_is_cancelled(error))
+        return;
+
+    /* Change state to FAILED unless already done by state_changed
+     * which may have been triggered by the station interface
+     * appearing on DBus.
+     */
+    if (nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG)
+        nm_device_queue_state(device,
+                              NM_DEVICE_STATE_FAILED,
+                              NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+}
+
+static void
+act_start_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *       self           = user_data;
+    NMDeviceIwdPrivate *priv           = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device         = NM_DEVICE(self);
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+    gs_free char *        ssid         = NULL;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant) {
+        _LOGE(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) {AccessPoint,AdHoc}.Start() failed: %s",
+              error->message);
+
+        if (nm_utils_error_is_cancelled(error))
+            return;
+
+        if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG))
+            return;
+
+        goto error;
+    }
+
+    nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_CONFIG);
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(nm_device_get_applied_connection(device),
+                                                      &ssid,
+                                                      NULL))
+        goto error;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) Stage 2 of 5 (Device Configure) successful.  Started '%s'.",
+          ssid);
+    nm_device_activate_schedule_stage3_ip_config_start(device);
+
+    return;
+
+error:
+    reset_mode(self, priv->cancellable, act_failed_cb, self);
+}
+
+/* Check if we're activating an AP/AdHoc connection and if the target
+ * DBus interface has appeared already.  If so proceed to call Start or
+ * StartOpen on that interface.
+ */
+static void
+act_check_interface(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate * priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+    NMSettingWireless *  s_wireless;
+    GDBusProxy *         proxy = NULL;
+    gs_free char *       ssid  = NULL;
+    const char *         mode;
+    NMIwdNetworkSecurity security;
+
+    if (!priv->act_mode_switch)
+        return;
+
+    s_wireless =
+        (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS);
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP))
+        proxy = priv->dbus_ap_proxy;
+    else if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_ADHOC))
+        proxy = priv->dbus_adhoc_proxy;
+
+    if (!proxy)
+        return;
+
+    priv->act_mode_switch = FALSE;
+
+    if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG))
+        return;
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(nm_device_get_applied_connection(device),
+                                                      &ssid,
+                                                      &security))
+        goto failed;
+
+    if (security == NM_IWD_NETWORK_SECURITY_OPEN) {
+        g_dbus_proxy_call(proxy,
+                          "StartOpen",
+                          g_variant_new("(s)", ssid),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          G_MAXINT,
+                          priv->cancellable,
+                          act_start_cb,
+                          self);
+    } else if (security == NM_IWD_NETWORK_SECURITY_PSK) {
+        NMSettingWirelessSecurity *s_wireless_sec;
+        const char *               psk;
+
+        s_wireless_sec = (NMSettingWirelessSecurity *) nm_device_get_applied_setting(
+            device,
+            NM_TYPE_SETTING_WIRELESS_SECURITY);
+        psk = nm_setting_wireless_security_get_psk(s_wireless_sec);
+
+        if (!psk) {
+            _LOGE(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) No PSK for '%s'.", ssid);
+            goto failed;
+        }
+
+        g_dbus_proxy_call(proxy,
+                          "Start",
+                          g_variant_new("(ss)", ssid, psk),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          G_MAXINT,
+                          priv->cancellable,
+                          act_start_cb,
+                          self);
+    } else
+        goto failed;
+
+    _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) Called Start('%s').", ssid);
+    return;
+
+failed:
+    reset_mode(self, priv->cancellable, act_failed_cb, self);
+}
+
+static void
+act_set_mode_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMDeviceIwd *       self           = user_data;
+    NMDeviceIwdPrivate *priv           = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device         = NM_DEVICE(self);
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant) {
+        _LOGE(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) Setting Device.Mode failed: %s",
+              error->message);
+
+        if (nm_utils_error_is_cancelled(error))
+            return;
+
+        if (!NM_IN_SET(nm_device_get_state(device), NM_DEVICE_STATE_CONFIG)
+            || !priv->act_mode_switch)
+            return;
+
+        priv->act_mode_switch = FALSE;
+        nm_device_queue_state(device,
+                              NM_DEVICE_STATE_FAILED,
+                              NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) IWD Device.Mode set successfully");
+
+    act_check_interface(self);
+}
+
+static void
+act_set_mode(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device = NM_DEVICE(self);
+    const char *        iwd_mode;
+    const char *        mode;
+    NMSettingWireless * s_wireless;
+
+    s_wireless =
+        (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS);
+    mode = nm_setting_wireless_get_mode(s_wireless);
+
+    /* We need to first set interface mode (Device.Mode) to ap or ad-hoc.
+     * We can't directly queue a call to the Start/StartOpen method on
+     * the DBus interface that's going to be created after the property
+     * set call returns.
+     */
+    iwd_mode = nm_streq(mode, NM_SETTING_WIRELESS_MODE_AP) ? "ap" : "ad-hoc";
+
+    if (!priv->cancellable)
+        priv->cancellable = g_cancellable_new();
+
+    g_dbus_proxy_call(
+        priv->dbus_device_proxy,
+        DBUS_INTERFACE_PROPERTIES ".Set",
+        g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Mode", g_variant_new("s", iwd_mode)),
+        G_DBUS_CALL_FLAGS_NONE,
+        2000,
+        priv->cancellable,
+        act_set_mode_cb,
+        self);
+    priv->act_mode_switch = TRUE;
+}
+
+static void
+act_psk_cb(NMActRequest *                req,
+           NMActRequestGetSecretsCallId *call_id,
+           NMSettingsConnection *        s_connection,
+           GError *                      error,
+           gpointer                      user_data)
+{
+    NMDeviceIwd *       self = user_data;
+    NMDeviceIwdPrivate *priv;
+    NMDevice *          device;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    device = NM_DEVICE(self);
+
+    g_return_if_fail(priv->wifi_secrets_id == call_id);
+    priv->wifi_secrets_id = NULL;
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == s_connection);
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_NEED_AUTH)
+        goto secrets_error;
+
+    if (error) {
+        _LOGW(LOGD_WIFI, "%s", error->message);
+        goto secrets_error;
+    }
+
+    _LOGD(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) missing PSK request completed");
+
+    /* Change state back to what it was before NEED_AUTH */
+    nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+    act_set_mode(self);
+    return;
+
+secrets_error:
+    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+    cleanup_association_attempt(self, FALSE);
+}
+
+static void
+set_powered(NMDeviceIwd *self, gboolean powered)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    g_dbus_proxy_call(
+        priv->dbus_device_proxy,
+        DBUS_INTERFACE_PROPERTIES ".Set",
+        g_variant_new("(ssv)", NM_IWD_DEVICE_INTERFACE, "Powered", g_variant_new("b", powered)),
+        G_DBUS_CALL_FLAGS_NONE,
+        2000,
+        NULL,
+        NULL,
+        NULL);
+}
+
+/*****************************************************************************/
+
+static NMWifiAP *
+find_ap_by_supplicant_path(NMDeviceIwd *self, const NMRefString *path)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMWifiAP *          tmp;
+
+    c_list_for_each_entry (tmp, &priv->aps_lst_head, aps_lst)
+        if (nm_wifi_ap_get_supplicant_path(tmp) == path)
+            return tmp;
+
+    return NULL;
+}
+
+static void
+assumed_connection_state_changed(NMActiveConnection *active, GParamSpec *pspec, NMDeviceIwd *self)
+{
+    NMSettingsConnection *  sett_conn = nm_active_connection_get_settings_connection(active);
+    NMActiveConnectionState state     = nm_active_connection_get_state(active);
+
+    /* Delete the temporary connection created for an external IWD connection
+     * (triggered by somebody outside of NM, be it IWD autoconnect or a
+     * parallel client), unless it's been referenced by a Known Network
+     * object since, which would remove the EXTERNAL flag.
+     *
+     * Note we can't do this too early, e.g. at the same time that we're
+     * setting the device state to FAILED or DISCONNECTING because the
+     * connection shouldn't disappear while it's still being used.  We do
+     * this on the connection's transition to DEACTIVATED same as as
+     * NMManager does for external activations.
+     */
+    if (state != NM_ACTIVE_CONNECTION_STATE_DEACTIVATED)
+        return;
+
+    g_signal_handlers_disconnect_by_func(active, assumed_connection_state_changed, NULL);
+
+    if (sett_conn
+        && NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                        NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL))
+        nm_settings_connection_delete(sett_conn, FALSE);
+}
+
+static void
+assumed_connection_state_changed_before_managed(NMActiveConnection *active,
+                                                GParamSpec *        pspec,
+                                                NMDeviceIwd *       self)
+{
+    NMDeviceIwdPrivate *    priv  = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMActiveConnectionState state = nm_active_connection_get_state(active);
+    gboolean                disconnect;
+
+    if (state != NM_ACTIVE_CONNECTION_STATE_DEACTIVATED)
+        return;
+
+    /* When an assumed connection fails we always get called, even if the
+     * activation hasn't reached PREPARE or CONFIG, e.g. because of a policy
+     * or authorization problem in NMManager.  .deactivate would only be
+     * called starting at some stage so we can't rely on that.
+     *
+     * If the error happened before PREPARE (where we set a non-NULL
+     * priv->current_ap) that will mean NM is somehow blocking autoconnect
+     * so we want to call IWD's Station.Disconnect() to block its
+     * autoconnect.  If this happens during or after PREPARE, we just
+     * clean up and wait for a new attempt by IWD.
+     *
+     * cleanup_association_attempt will clear priv->assumed_ac, disconnect
+     * this callback from the signal and also send a Disconnect to IWD if
+     * needed.
+     *
+     * Note this function won't be called after IWD transitions to
+     * "connected" (and NMDevice to IP_CONFIG) as we disconnect from the
+     * signal at that point, cleanup_association_attempt() will be
+     * triggered by an IWD state change instead.
+     */
+    disconnect = !priv->current_ap;
+    cleanup_association_attempt(self, disconnect);
+}
+
+static void
+assume_connection(NMDeviceIwd *self, NMWifiAP *ap)
+{
+    NMDeviceIwdPrivate *  priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMSettingsConnection *sett_conn;
+    gs_unref_object NMAuthSubject *subject = NULL;
+    NMActiveConnection *           ac;
+    gs_free_error GError *error = NULL;
+
+    /* We can use the .update_connection / nm_device_emit_recheck_assume
+     * API but we can also pass an assumed/external activation type
+     * directly to nm_manager_activate_connection() and skip the
+     * complicated process of creating a matching connection, taking
+     * advantage of the Known Networks pointing directly to a mirror
+     * connection.  The only downside seems to be
+     * nm_manager_activate_connection() goes through the extra
+     * authorization.
+     *
+     * However for now we implement a similar behaviour using a normal
+     * "managed" activation.  For one, assumed/external
+     * connection state is not reflected in nm_manager_get_state() until
+     * fully activated.  Secondly setting the device state to FAILED
+     * is treated as ACTIVATED so we'd have to find another way to signal
+     * that stage2 is failing asynchronously.  Thirdly the connection
+     * becomes "managed" only when ACTIVATED but for IWD it's really
+     * managed when IP_CONFIG starts.
+     */
+    sett_conn = nm_iwd_manager_get_ap_mirror_connection(nm_iwd_manager_get(), ap);
+    if (!sett_conn)
+        goto error;
+
+    subject = nm_auth_subject_new_internal();
+    ac      = nm_manager_activate_connection(
+        NM_MANAGER_GET,
+        sett_conn,
+        NULL,
+        nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)),
+        NM_DEVICE(self),
+        subject,
+        NM_ACTIVATION_TYPE_MANAGED,
+        NM_ACTIVATION_REASON_ASSUME,
+        NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY,
+        &error);
+
+    if (!ac) {
+        _LOGW(LOGD_WIFI, "Activation: (wifi) assume error: %s", error->message);
+        goto error;
+    }
+
+    /* If no Known Network existed for this AP, we generated a temporary
+     * NMSettingsConnection with the EXTERNAL flag.  It is not referenced by
+     * any Known Network objects at this time so we want to delete it if the
+     * IWD connection ends up failing or a later part of the activation fails
+     * before IWD created a Known Network.
+     * Setting the activation type to EXTERNAL would do this by causing
+     * NM_ACTIVATION_STATE_FLAG_EXTERNAL to be set on the NMActiveConnection
+     * but we don't want the connection to be marked EXTERNAL because we
+     * will be assuming the ownership of it in IP_CONFIG or thereabouts.
+     *
+     * This callback stays connected forever while the second one gets
+     * disconnected when we reset the activation type to managed.
+     */
+    g_signal_connect(ac,
+                     "notify::" NM_ACTIVE_CONNECTION_STATE,
+                     G_CALLBACK(assumed_connection_state_changed),
+                     NULL);
+    g_signal_connect(ac,
+                     "notify::" NM_ACTIVE_CONNECTION_STATE,
+                     G_CALLBACK(assumed_connection_state_changed_before_managed),
+                     self);
+    priv->assumed_ac = g_object_ref(ac);
+
+    return;
+
+error:
+    send_disconnect(self);
+
+    if (sett_conn
+        && NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                        NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL))
+        nm_settings_connection_delete(sett_conn, FALSE);
+}
+
+static void
+assumed_connection_progress_to_ip_config(NMDeviceIwd *self, gboolean was_postponed)
+{
+    NMDeviceIwdPrivate *priv      = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device    = NM_DEVICE(self);
+    NMDeviceState       dev_state = nm_device_get_state(device);
+
+    wifi_secrets_cancel(self);
+    nm_clear_g_source(&priv->assumed_ac_timeout);
+
+    /* NM takes over the activation from this point on so clear the assumed
+     * activation state and if we were using NM_ACTIVATION_TYPE_ASSUMED or
+     * _EXTERNAL we'd need to reset the activation type to _MANAGED at this
+     * point instead of waiting for the ACTIVATED state (as done in
+     * nm_active_connection_set_state).
+     */
+    cleanup_assumed_connect(self);
+
+    if (dev_state == NM_DEVICE_STATE_NEED_AUTH)
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_CONFIG,
+                                NM_DEVICE_STATE_REASON_NONE);
+
+    /* If stage2 had returned NM_ACT_STAGE_RETURN_POSTPONE, we tell NMDevice
+     * that stage2 is done.
+     */
+    if (was_postponed)
+        nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self));
+}
+
+static void
+initial_check_assume(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const char *        network_path_str;
+    nm_auto_ref_string NMRefString *network_path = NULL;
+    NMWifiAP *                      ap           = NULL;
+    gs_unref_variant GVariant *state_value =
+        g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+    gs_unref_variant GVariant *cn_value =
+        g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "ConnectedNetwork");
+
+    if (!NM_IN_STRSET(get_variant_state(state_value), "connecting", "connected", "roaming"))
+        return;
+
+    if (!priv->iwd_autoconnect) {
+        send_disconnect(self);
+        return;
+    }
+
+    if (!cn_value || !g_variant_is_of_type(cn_value, G_VARIANT_TYPE_OBJECT_PATH)) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "ConnectedNetwork property not cached or not an object path");
+        return;
+    }
+
+    network_path_str = g_variant_get_string(cn_value, NULL);
+    network_path     = nm_ref_string_new(network_path_str);
+    ap               = find_ap_by_supplicant_path(self, network_path);
+
+    if (!ap) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "ConnectedNetwork points to an unknown Network %s",
+              network_path_str);
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE | LOGD_WIFI, "assuming connection in initial_check_assume");
+    assume_connection(self, ap);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceIwd *       self          = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv          = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMWifiAP *          ap            = NULL;
+    gs_unref_object NMWifiAP *ap_fake = NULL;
+    NMActRequest *            req;
+    NMConnection *            connection;
+    NMSettingWireless *       s_wireless;
+    const char *              mode;
+    const char *              ap_path;
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE);
+
+    /* AP, Ad-Hoc modes never use a specific object or existing scanned AP */
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC))
+        goto add_new;
+
+    ap_path = nm_active_connection_get_specific_object(NM_ACTIVE_CONNECTION(req));
+    ap      = ap_path ? nm_wifi_ap_lookup_for_device(NM_DEVICE(self), ap_path) : NULL;
+    if (ap) {
+        set_current_ap(self, ap, TRUE);
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+    if (ap) {
+        nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req),
+                                                 nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)));
+        set_current_ap(self, ap, TRUE);
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+    }
+
+    /* In infrastructure mode the specific object should be set by now except
+     * for a first-time connection to a hidden network.  If a hidden network is
+     * a Known Network it should still have been in the AP list.
+     */
+    if (!nm_setting_wireless_get_hidden(s_wireless) || is_connection_known_network(connection))
+        return NM_ACT_STAGE_RETURN_FAILURE;
+
+add_new:
+    /* If the user is trying to connect to an AP that NM doesn't yet know about
+     * (hidden network or something) or starting a Hotspot, create a fake AP
+     * from the security settings in the connection.  This "fake" AP gets used
+     * until the real one is found in the scan list (Ad-Hoc or Hidden), or until
+     * the device is deactivated (Ad-Hoc or Hotspot).
+     */
+    ap_fake = nm_wifi_ap_new_fake_from_connection(connection);
+    if (!ap_fake)
+        g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (nm_wifi_ap_is_hotspot(ap_fake))
+        nm_wifi_ap_set_address(ap_fake, nm_device_get_hw_address(device));
+
+    g_object_freeze_notify(G_OBJECT(self));
+    ap_add_remove(self, TRUE, ap_fake, FALSE);
+    g_object_thaw_notify(G_OBJECT(self));
+    set_current_ap(self, ap_fake, FALSE);
+    nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req),
+                                             nm_dbus_object_get_path(NM_DBUS_OBJECT(ap_fake)));
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMActRequest *      req;
+    NMConnection *      connection;
+    NMSettingWireless * s_wireless;
+    const char *        mode;
+
+    req        = nm_device_get_act_request(device);
+    connection = nm_act_request_get_applied_connection(req);
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE);
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+
+    if (NM_IN_STRSET(mode, NULL, NM_SETTING_WIRELESS_MODE_INFRA)) {
+        gs_unref_object GDBusProxy *network_proxy = NULL;
+        NMWifiAP *                  ap            = priv->current_ap;
+        NMSettingWirelessSecurity * s_wireless_sec;
+
+        if (!ap) {
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+            goto out_fail;
+        }
+
+        /* With priv->iwd_autoconnect, if we're assuming a connection because
+         * of a state change to "connecting", signal stage 2 is still running.
+         * If "connected" or "roaming", we can go right to the IP_CONFIG state
+         * and there's nothing left to do in CONFIG.
+         * If we're assuming the connection because of an agent request we
+         * switch to NEED_AUTH and actually send the request now that we
+         * have an activation request.
+         *
+         * This all assumes ConnectedNetwork hasn't changed.
+         */
+        if (priv->assumed_ac) {
+            gboolean result;
+
+            if (!priv->pending_agent_request) {
+                gs_unref_variant GVariant *value =
+                    g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+
+                if (nm_streq(get_variant_state(value), "connecting")) {
+                    return NM_ACT_STAGE_RETURN_POSTPONE;
+                } else {
+                    /* This basically forgets that the connection was "assumed"
+                     * as we can treat it like any connection triggered by a
+                     * Network.Connect() call from now on.
+                     */
+                    assumed_connection_progress_to_ip_config(self, FALSE);
+                    return NM_ACT_STAGE_RETURN_SUCCESS;
+                }
+            }
+
+            result = nm_device_iwd_agent_query(self, priv->pending_agent_request);
+            g_clear_object(&priv->pending_agent_request);
+            nm_assert(result);
+
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+
+        /* 802.1x networks that are not IWD Known Networks will definitely
+         * fail, for other combinations we will let the Connect call fail
+         * or ask us for any missing secrets through the Agent.
+         */
+        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(ap)) {
+            _LOGI(LOGD_DEVICE | LOGD_WIFI,
+                  "Activation: (wifi) access point '%s' has 802.1x security but is not configured "
+                  "in IWD.",
+                  nm_connection_get_id(connection));
+
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+            goto out_fail;
+        }
+
+        priv->secrets_failed = FALSE;
+
+        if (nm_wifi_ap_get_fake(ap)) {
+            gs_free char *ssid = NULL;
+
+            if (!nm_setting_wireless_get_hidden(s_wireless)) {
+                _LOGW(LOGD_DEVICE | LOGD_WIFI,
+                      "Activation: (wifi) target network not known to IWD but is not "
+                      "marked hidden");
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+                goto out_fail;
+            }
+
+            if (!nm_wifi_connection_get_iwd_ssid_and_security(connection, &ssid, NULL)) {
+                NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+                goto out_fail;
+            }
+
+            /* Use Station.ConnectHiddenNetwork method instead of Network proxy. */
+            g_dbus_proxy_call(priv->dbus_station_proxy,
+                              "ConnectHiddenNetwork",
+                              g_variant_new("(s)", ssid),
+                              G_DBUS_CALL_FLAGS_NONE,
+                              G_MAXINT,
+                              priv->cancellable,
+                              network_connect_cb,
+                              self);
+            return NM_ACT_STAGE_RETURN_POSTPONE;
+        }
+
+        network_proxy = nm_iwd_manager_get_dbus_interface(
+            nm_iwd_manager_get(),
+            nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
+            NM_IWD_NETWORK_INTERFACE);
+        if (!network_proxy) {
+            _LOGW(LOGD_DEVICE | LOGD_WIFI,
+                  "Activation: (wifi) could not get Network interface proxy for %s",
+                  nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)));
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+            goto out_fail;
+        }
+
+        if (!priv->cancellable)
+            priv->cancellable = g_cancellable_new();
+
+        s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+        if (s_wireless_sec
+            && nm_streq0(nm_setting_wireless_security_get_key_mgmt(s_wireless_sec), "owe")) {
+            _LOGI(LOGD_WIFI,
+                  "An OWE connection is requested but IWD may connect to either an OWE "
+                  "or unsecured network and there won't be any indication of whether "
+                  "encryption is in use -- proceed at your own risk!");
+        }
+
+        /* Call Network.Connect.  No timeout because IWD already handles
+         * timeouts.
+         */
+        g_dbus_proxy_call(network_proxy,
+                          "Connect",
+                          NULL,
+                          G_DBUS_CALL_FLAGS_NONE,
+                          G_MAXINT,
+                          priv->cancellable,
+                          network_connect_cb,
+                          self);
+
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (NM_IN_STRSET(mode, NM_SETTING_WIRELESS_MODE_AP, NM_SETTING_WIRELESS_MODE_ADHOC)) {
+        NMSettingWirelessSecurity *s_wireless_sec;
+
+        s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+        if (s_wireless_sec && !nm_setting_wireless_security_get_psk(s_wireless_sec)) {
+            /* PSK is missing from the settings, have to request it */
+
+            wifi_secrets_cancel(self);
+
+            priv->wifi_secrets_id =
+                nm_act_request_get_secrets(req,
+                                           TRUE,
+                                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                           NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION,
+                                           NM_MAKE_STRV(NM_SETTING_WIRELESS_SECURITY_PSK),
+                                           act_psk_cb,
+                                           self);
+            nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
+        } else
+            act_set_mode(self);
+
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) iwd cannot handle mode %s", mode);
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+
+out_fail:
+    cleanup_association_attempt(self, FALSE);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_WIRELESS,
+                                                        out_source);
+}
+
+static gboolean
+periodic_scan_timeout_cb(gpointer user_data)
+{
+    NMDeviceIwd *       self = user_data;
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    priv->periodic_scan_id = 0;
+
+    if (priv->scanning || priv->scan_requested)
+        return FALSE;
+
+    g_dbus_proxy_call(priv->dbus_station_proxy,
+                      "Scan",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      -1,
+                      priv->cancellable,
+                      scan_cb,
+                      self);
+    priv->scan_requested = TRUE;
+
+    return FALSE;
+}
+
+static void
+schedule_periodic_scan(NMDeviceIwd *self, gboolean initial_scan)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    guint               interval;
+
+    /* Automatically start a scan after a disconnect, mode change or device UP,
+     * otherwise scan periodically every 10 seconds if needed for NM's
+     * autoconnect.  There's no need to scan When using IWD's autoconnect or
+     * when connected, we update the AP list on UI requests.
+     *
+     * (initial_scan && disconnected && !priv->iwd_autoconnect) override
+     * priv->scanning below because of an IWD quirk where a device will often
+     * be in the autoconnect state and scanning at the time of our initial_scan,
+     * but our logic will then send it a Disconnect() causing IWD to exit
+     * autoconnect and interrupt the ongoing scan, meaning that we still want
+     * a new scan ASAP.
+     */
+    if (!priv->can_scan || priv->scan_requested || priv->current_ap || priv->iwd_autoconnect)
+        interval = -1;
+    else if (initial_scan && priv->scanning)
+        interval = 0;
+    else if (priv->scanning)
+        interval = -1;
+    else if (!priv->periodic_scan_id)
+        interval = 10;
+    else
+        return;
+
+    nm_clear_g_source(&priv->periodic_scan_id);
+
+    if (interval != (guint) -1)
+        priv->periodic_scan_id = g_timeout_add_seconds(interval, periodic_scan_timeout_cb, self);
+}
+
+static void
+set_can_scan(NMDeviceIwd *self, gboolean can_scan)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (priv->can_scan == can_scan)
+        return;
+
+    priv->can_scan = can_scan;
+
+    if (!priv->iwd_autoconnect)
+        schedule_periodic_scan(self, TRUE);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMSettingWireless * s_wireless;
+    const char *        mode;
+
+    switch (new_state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+        break;
+    case NM_DEVICE_STATE_UNAVAILABLE:
+        /*
+         * If the device is enabled and the IWD manager is ready,
+         * transition to DISCONNECTED because the device is now
+         * ready to use.
+         */
+        if (priv->enabled && priv->dbus_station_proxy) {
+            nm_device_queue_recheck_available(device,
+                                              NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                              NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        }
+        break;
+    case NM_DEVICE_STATE_DISCONNECTED:
+        if (old_state == NM_DEVICE_STATE_UNAVAILABLE)
+            initial_check_assume(self);
+        break;
+    case NM_DEVICE_STATE_IP_CONFIG:
+        s_wireless =
+            (NMSettingWireless *) nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS);
+        mode = nm_setting_wireless_get_mode(s_wireless);
+        if (!priv->periodic_update_id
+            && NM_IN_STRSET(mode,
+                            NULL,
+                            NM_SETTING_WIRELESS_MODE_INFRA,
+                            NM_SETTING_WIRELESS_MODE_ADHOC)) {
+            priv->periodic_update_id = g_timeout_add_seconds(6, periodic_update_cb, self);
+            periodic_update(self);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static gboolean
+get_enabled(NMDevice *device)
+{
+    return NM_DEVICE_IWD_GET_PRIVATE(device)->enabled;
+}
+
+static void
+set_enabled(NMDevice *device, gboolean enabled)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDeviceState       state;
+
+    enabled = !!enabled;
+
+    if (priv->enabled == enabled)
+        return;
+
+    priv->enabled = enabled;
+
+    _LOGD(LOGD_WIFI, "device now %s", enabled ? "enabled" : "disabled");
+
+    state = nm_device_get_state(device);
+    if (state < NM_DEVICE_STATE_UNAVAILABLE) {
+        _LOGD(LOGD_WIFI, "(%s): device blocked by UNMANAGED state", enabled ? "enable" : "disable");
+        return;
+    }
+
+    if (priv->dbus_obj)
+        set_powered(self, enabled);
+
+    if (enabled) {
+        if (state != NM_DEVICE_STATE_UNAVAILABLE)
+            _LOGW(LOGD_CORE, "not in expected unavailable state!");
+
+        if (priv->dbus_station_proxy) {
+            nm_device_queue_recheck_available(device,
+                                              NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                              NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        }
+    } else {
+        nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE);
+    }
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    /* Only handle wireless setting here, delegate other settings to parent class */
+    if (nm_streq(setting_name, NM_SETTING_WIRELESS_SETTING_NAME)) {
+        return nm_device_hash_check_invalid_keys(
+            diffs,
+            NM_SETTING_WIRELESS_SETTING_NAME,
+            error,
+            NM_SETTING_WIRELESS_SEEN_BSSIDS, /* ignored */
+            NM_SETTING_WIRELESS_MTU);        /* reapplied with IP config */
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_iwd_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(object);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const char **       list;
+
+    switch (prop_id) {
+    case PROP_MODE:
+        if (!priv->current_ap)
+            g_value_set_uint(value, NM_802_11_MODE_UNKNOWN);
+        else if (nm_wifi_ap_is_hotspot(priv->current_ap))
+            g_value_set_uint(value, NM_802_11_MODE_AP);
+        else
+            g_value_set_uint(value, nm_wifi_ap_get_mode(priv->current_ap));
+
+        break;
+    case PROP_BITRATE:
+        g_value_set_uint(value, priv->rate);
+        break;
+    case PROP_CAPABILITIES:
+        g_value_set_uint(value, priv->capabilities);
+        break;
+    case PROP_ACCESS_POINTS:
+        list = nm_wifi_aps_get_paths(&priv->aps_lst_head, TRUE);
+        g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list));
+        break;
+    case PROP_ACTIVE_ACCESS_POINT:
+        nm_dbus_utils_g_value_set_object_path(value, priv->current_ap);
+        break;
+    case PROP_SCANNING:
+        g_value_set_boolean(value, priv->scanning);
+        break;
+    case PROP_LAST_SCAN:
+        g_value_set_int64(
+            value,
+            priv->last_scan > 0
+                ? nm_utils_monotonic_timestamp_as_boottime(priv->last_scan, NM_UTILS_NSEC_PER_MSEC)
+                : (gint64) -1);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+state_changed(NMDeviceIwd *self, const char *new_state)
+{
+    NMDeviceIwdPrivate *priv           = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDevice *          device         = NM_DEVICE(self);
+    NMDeviceState       dev_state      = nm_device_get_state(device);
+    gboolean            nm_connection  = priv->current_ap || priv->assumed_ac;
+    gboolean            iwd_connection = FALSE;
+    NMWifiAP *          ap             = NULL;
+    gboolean            can_connect    = priv->nm_autoconnect;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI, "new IWD device state is %s", new_state);
+
+    if (NM_IN_STRSET(new_state, "connecting", "connected", "roaming")) {
+        gs_unref_variant GVariant *value = NULL;
+        const char *               network_path_str;
+        nm_auto_ref_string NMRefString *network_path = NULL;
+
+        value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "ConnectedNetwork");
+        if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) {
+            _LOGW(LOGD_DEVICE | LOGD_WIFI,
+                  "ConnectedNetwork property not cached or not an object path");
+            return;
+        }
+
+        iwd_connection   = TRUE;
+        network_path_str = g_variant_get_string(value, NULL);
+        network_path     = nm_ref_string_new(network_path_str);
+        ap               = find_ap_by_supplicant_path(self, network_path);
+
+        if (!ap) {
+            _LOGW(LOGD_DEVICE | LOGD_WIFI,
+                  "ConnectedNetwork points to an unknown Network %s",
+                  network_path_str);
+            return;
+        }
+    }
+
+    /* Don't allow scanning while connecting, disconnecting or roaming */
+    set_can_scan(self, NM_IN_STRSET(new_state, "connected", "disconnected"));
+
+    priv->nm_autoconnect = FALSE;
+
+    if (nm_connection && iwd_connection && priv->current_ap && ap != priv->current_ap) {
+        gboolean switch_ap = priv->iwd_autoconnect && priv->assumed_ac;
+
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "IWD is connecting to the wrong AP, %s activation",
+              switch_ap ? "replacing" : "aborting");
+        cleanup_association_attempt(self, !switch_ap);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+
+        if (switch_ap)
+            assume_connection(self, ap);
+        return;
+    }
+
+    if (priv->iwd_autoconnect && iwd_connection) {
+        if (dev_state < NM_DEVICE_STATE_DISCONNECTED)
+            return;
+
+        /* If IWD is in any state other than disconnected and the NMDevice is
+         * in DISCONNECTED then someone else, possibly IWD's autoconnect, has
+         * commanded an action and we need to update our NMDevice's state to
+         * match, including finding the NMSettingsConnection and NMWifiAP
+         * matching the network pointed to by Station.ConnectedNetwork.
+         *
+         * If IWD is in the connected state and we're in CONFIG, we only have
+         * to signal that the existing connection request has advanced to a new
+         * state.  If the connection request came from NM, we must have used
+         * Network.Connect() so that method call's callback will update the
+         * connection request, otherwise we do it here.
+         *
+         * If IWD is disconnecting or just disconnected, the common code below
+         * (independent from priv->iwd_autoconnect) will handle this case.
+         * If IWD is disconnecting but we never saw a connection request in the
+         * first place (maybe because we're only startig up) we won't be
+         * setting up an NMActiveConnection just to put the NMDevice in the
+         * DEACTIVATING state and we ignore this case.
+         *
+         * If IWD was in the disconnected state and transitioned to
+         * "connecting" but we were already in NEED_AUTH because we handled an
+         * agent query -- IWD normally stays in "disconnected" until it has all
+         * the secrets -- we record this fact and remain in NEED_AUTH.
+         */
+        if (!nm_connection) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "This is a new connection, 'assuming' it");
+            assume_connection(self, ap);
+            return;
+        }
+
+        if (priv->assumed_ac && dev_state >= NM_DEVICE_STATE_PREPARE
+            && dev_state < NM_DEVICE_STATE_IP_CONFIG
+            && NM_IN_STRSET(new_state, "connected", "roaming")) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Updating assumed activation state");
+            assumed_connection_progress_to_ip_config(self, TRUE);
+            return;
+        }
+
+        if (priv->assumed_ac) {
+            _LOGD(LOGD_DEVICE | LOGD_WIFI, "Clearing assumed activation timeout");
+            nm_clear_g_source(&priv->assumed_ac_timeout);
+            return;
+        }
+    } else if (!priv->iwd_autoconnect && iwd_connection) {
+        /* If we were connecting, do nothing, the confirmation of
+         * a connection success is handled in the Device.Connect
+         * method return callback.  Otherwise, IWD must have connected
+         * without Network Manager's will so for simplicity force a
+         * disconnect.
+         */
+        if (nm_connection)
+            return;
+
+        _LOGW(LOGD_DEVICE | LOGD_WIFI, "Unsolicited connection, asking IWD to disconnect");
+        send_disconnect(self);
+    } else if (NM_IN_STRSET(new_state, "disconnecting", "disconnected")) {
+        /* If necessary, call Disconnect on the IWD device object to make sure
+         * it disables its autoconnect.
+         */
+        if ((!priv->iwd_autoconnect
+             || nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL))
+            && !priv->wifi_secrets_id && !priv->pending_agent_request)
+            send_disconnect(self);
+
+        /*
+         * If IWD is still handling the Connect call, let our Connect
+         * callback for the dbus method handle the failure.  The main
+         * reason we don't want to handle the failure here is because the
+         * method callback will have more information on the specific
+         * failure reason.
+         *
+         * If IWD is handling an autoconnect agent call, let the agent's
+         * Cancel() handler take care of this.
+         */
+        if (NM_IN_SET(dev_state, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_NEED_AUTH)
+            && !priv->assumed_ac)
+            return;
+        if (NM_IN_SET(dev_state, NM_DEVICE_STATE_NEED_AUTH) && priv->assumed_ac)
+            return;
+
+        if (nm_connection) {
+            cleanup_association_attempt(self, FALSE);
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+        }
+    } else if (!nm_streq(new_state, "unknown")) {
+        _LOGE(LOGD_WIFI, "State %s unknown", new_state);
+        return;
+    }
+
+    /* Don't allow new connection until iwd exits disconnecting and no
+     * Connect callback is pending.
+     */
+    if (!priv->iwd_autoconnect && NM_IN_STRSET(new_state, "disconnected")) {
+        priv->nm_autoconnect = TRUE;
+        if (!can_connect)
+            nm_device_emit_recheck_auto_activate(device);
+    }
+}
+
+static void
+scanning_changed(NMDeviceIwd *self, gboolean new_scanning)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    if (new_scanning == priv->scanning)
+        return;
+
+    priv->scanning = new_scanning;
+
+    _notify(self, PROP_SCANNING);
+
+    if (!priv->scanning) {
+        update_aps(self);
+
+        if (!priv->scan_requested && !priv->iwd_autoconnect)
+            schedule_periodic_scan(self, FALSE);
+    }
+}
+
+static void
+station_properties_changed(GDBusProxy *proxy,
+                           GVariant *  changed_properties,
+                           GStrv       invalidate_properties,
+                           gpointer    user_data)
+{
+    NMDeviceIwd *self = user_data;
+    const char * new_str;
+    gboolean     new_bool;
+
+    if (g_variant_lookup(changed_properties, "State", "&s", &new_str))
+        state_changed(self, new_str);
+
+    if (g_variant_lookup(changed_properties, "Scanning", "b", &new_bool))
+        scanning_changed(self, new_bool);
+}
+
+static void
+ap_adhoc_properties_changed(GDBusProxy *proxy,
+                            GVariant *  changed_properties,
+                            GStrv       invalidate_properties,
+                            gpointer    user_data)
+{
+    NMDeviceIwd *self = user_data;
+    gboolean     new_bool;
+
+    if (g_variant_lookup(changed_properties, "Started", "b", &new_bool))
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "IWD AP/AdHoc state is now %s",
+              new_bool ? "Started" : "Stopped");
+}
+
+static void
+powered_changed(NMDeviceIwd *self, gboolean new_powered)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    GDBusInterface *    interface;
+
+    nm_device_queue_recheck_available(NM_DEVICE(self),
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+
+    interface =
+        new_powered ? g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_AP_INTERFACE) : NULL;
+
+    if (priv->dbus_ap_proxy) {
+        g_signal_handlers_disconnect_by_func(priv->dbus_ap_proxy,
+                                             ap_adhoc_properties_changed,
+                                             self);
+        g_clear_object(&priv->dbus_ap_proxy);
+    }
+
+    if (interface) {
+        priv->dbus_ap_proxy = G_DBUS_PROXY(interface);
+        g_signal_connect(priv->dbus_ap_proxy,
+                         "g-properties-changed",
+                         G_CALLBACK(ap_adhoc_properties_changed),
+                         self);
+
+        if (priv->act_mode_switch)
+            act_check_interface(self);
+        else
+            reset_mode(self, NULL, NULL, NULL);
+    }
+
+    interface =
+        new_powered ? g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_ADHOC_INTERFACE) : NULL;
+
+    if (priv->dbus_adhoc_proxy) {
+        g_signal_handlers_disconnect_by_func(priv->dbus_adhoc_proxy,
+                                             ap_adhoc_properties_changed,
+                                             self);
+        g_clear_object(&priv->dbus_adhoc_proxy);
+    }
+
+    if (interface) {
+        priv->dbus_adhoc_proxy = G_DBUS_PROXY(interface);
+        g_signal_connect(priv->dbus_adhoc_proxy,
+                         "g-properties-changed",
+                         G_CALLBACK(ap_adhoc_properties_changed),
+                         self);
+
+        if (priv->act_mode_switch)
+            act_check_interface(self);
+        else
+            reset_mode(self, NULL, NULL, NULL);
+    }
+
+    /* We expect one of the three interfaces to always be present when
+     * device is Powered so if AP and AdHoc are not present we should
+     * be in station mode.
+     */
+    if (new_powered && !priv->dbus_ap_proxy && !priv->dbus_adhoc_proxy) {
+        interface = g_dbus_object_get_interface(priv->dbus_obj, NM_IWD_STATION_INTERFACE);
+        if (!interface) {
+            _LOGE(LOGD_WIFI,
+                  "Interface %s not found on obj %s",
+                  NM_IWD_STATION_INTERFACE,
+                  g_dbus_object_get_object_path(priv->dbus_obj));
+            interface = NULL;
+        }
+    } else
+        interface = NULL;
+
+    if (priv->dbus_station_proxy) {
+        g_signal_handlers_disconnect_by_func(priv->dbus_station_proxy,
+                                             station_properties_changed,
+                                             self);
+        g_clear_object(&priv->dbus_station_proxy);
+    }
+
+    if (interface) {
+        GVariant *value;
+
+        priv->dbus_station_proxy = G_DBUS_PROXY(interface);
+        g_signal_connect(priv->dbus_station_proxy,
+                         "g-properties-changed",
+                         G_CALLBACK(station_properties_changed),
+                         self);
+
+        value          = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "Scanning");
+        priv->scanning = get_variant_boolean(value, "Scanning");
+        g_variant_unref(value);
+
+        value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+        state_changed(self, get_variant_state(value));
+        g_variant_unref(value);
+
+        update_aps(self);
+
+        /* When a device is brought UP in station mode, including after a mode
+         * switch, IWD re-enables autoconnect.  This is unlike NM's autoconnect
+         * where a mode change doesn't interfere with the
+         * BLOCKED_MANUAL_DISCONNECT flag.
+         */
+        if (priv->iwd_autoconnect) {
+            nm_device_autoconnect_blocked_unset(NM_DEVICE(self),
+                                                NM_DEVICE_AUTOCONNECT_BLOCKED_INTERNAL);
+        }
+    } else {
+        set_can_scan(self, FALSE);
+        priv->scanning       = FALSE;
+        priv->scan_requested = FALSE;
+        priv->nm_autoconnect = FALSE;
+        cleanup_association_attempt(self, FALSE);
+        remove_all_aps(self);
+    }
+}
+
+static void
+device_properties_changed(GDBusProxy *proxy,
+                          GVariant *  changed_properties,
+                          GStrv       invalidate_properties,
+                          gpointer    user_data)
+{
+    NMDeviceIwd *self = user_data;
+    gboolean     new_bool;
+
+    if (g_variant_lookup(changed_properties, "Powered", "b", &new_bool))
+        powered_changed(self, new_bool);
+}
+
+static void
+config_changed(NMConfig *          config,
+               NMConfigData *      config_data,
+               NMConfigChangeFlags changes,
+               NMConfigData *      old_data,
+               NMDeviceIwd *       self)
+{
+    NMDeviceIwdPrivate *priv       = NM_DEVICE_IWD_GET_PRIVATE(self);
+    gboolean            old_iwd_ac = priv->iwd_autoconnect;
+
+    priv->iwd_autoconnect =
+        nm_config_data_get_device_config_boolean(config_data,
+                                                 NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_IWD_AUTOCONNECT,
+                                                 NM_DEVICE(self),
+                                                 TRUE,
+                                                 TRUE);
+
+    if (old_iwd_ac != priv->iwd_autoconnect && priv->dbus_station_proxy && !priv->current_ap) {
+        gs_unref_variant GVariant *value = NULL;
+
+        if (!priv->iwd_autoconnect
+            && !nm_device_autoconnect_blocked_get(NM_DEVICE(self),
+                                                  NM_DEVICE_AUTOCONNECT_BLOCKED_ALL))
+            send_disconnect(self);
+
+        value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+        state_changed(self, get_variant_state(value));
+    }
+}
+
+void
+nm_device_iwd_set_dbus_object(NMDeviceIwd *self, GDBusObject *object)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    GDBusInterface *    interface;
+    gs_unref_variant GVariant *value          = NULL;
+    gs_unref_object GDBusProxy *adapter_proxy = NULL;
+    GVariantIter *              iter;
+    const char *                mode;
+    gboolean                    powered;
+    NMDeviceWifiCapabilities    capabilities;
+
+    if (!nm_g_object_ref_set(&priv->dbus_obj, object))
+        return;
+
+    if (priv->dbus_device_proxy) {
+        g_signal_handlers_disconnect_by_func(priv->dbus_device_proxy,
+                                             device_properties_changed,
+                                             self);
+        g_clear_object(&priv->dbus_device_proxy);
+
+        powered_changed(self, FALSE);
+
+        priv->act_mode_switch = FALSE;
+
+        g_signal_handlers_disconnect_by_func(nm_config_get(), config_changed, self);
+    }
+
+    if (!object)
+        return;
+
+    interface = g_dbus_object_get_interface(object, NM_IWD_DEVICE_INTERFACE);
+    if (!interface) {
+        _LOGE(LOGD_WIFI,
+              "Interface %s not found on obj %s",
+              NM_IWD_DEVICE_INTERFACE,
+              g_dbus_object_get_object_path(object));
+        g_clear_object(&priv->dbus_obj);
+        return;
+    }
+
+    priv->dbus_device_proxy = G_DBUS_PROXY(interface);
+
+    g_signal_connect(priv->dbus_device_proxy,
+                     "g-properties-changed",
+                     G_CALLBACK(device_properties_changed),
+                     self);
+
+    /* Parse list of interface modes supported by adapter (wiphy) */
+
+    value = g_dbus_proxy_get_cached_property(priv->dbus_device_proxy, "Adapter");
+    if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) {
+        nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "Adapter property not cached or not an object path");
+        goto error;
+    }
+
+    adapter_proxy = nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+                                                      g_variant_get_string(value, NULL),
+                                                      NM_IWD_WIPHY_INTERFACE);
+    if (!adapter_proxy) {
+        nm_log_warn(LOGD_DEVICE | LOGD_WIFI, "Can't get DBus proxy for IWD Adapter for IWD Device");
+        goto error;
+    }
+
+    g_variant_unref(value);
+    value = g_dbus_proxy_get_cached_property(adapter_proxy, "SupportedModes");
+    if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY)) {
+        nm_log_warn(LOGD_DEVICE | LOGD_WIFI,
+                    "SupportedModes property not cached or not a string array");
+        goto error;
+    }
+
+    capabilities = NM_WIFI_DEVICE_CAP_CIPHER_CCMP | NM_WIFI_DEVICE_CAP_RSN;
+
+    g_variant_get(value, "as", &iter);
+    while (g_variant_iter_next(iter, "&s", &mode)) {
+        if (nm_streq(mode, "ap"))
+            capabilities |= NM_WIFI_DEVICE_CAP_AP;
+        else if (nm_streq(mode, "ad-hoc"))
+            capabilities |= NM_WIFI_DEVICE_CAP_ADHOC;
+    }
+    g_variant_iter_free(iter);
+
+    if (priv->capabilities != capabilities) {
+        priv->capabilities = capabilities;
+        _notify(self, PROP_CAPABILITIES);
+    }
+
+    /* Update iwd_autoconnect before any state_changed call */
+    g_signal_connect(nm_config_get(),
+                     NM_CONFIG_SIGNAL_CONFIG_CHANGED,
+                     G_CALLBACK(config_changed),
+                     self);
+    config_changed(NULL, NM_CONFIG_GET_DATA, 0, NULL, self);
+
+    g_variant_unref(value);
+    value   = g_dbus_proxy_get_cached_property(priv->dbus_device_proxy, "Powered");
+    powered = get_variant_boolean(value, "Powered");
+
+    if (powered != priv->enabled)
+        set_powered(self, priv->enabled);
+    else if (powered)
+        powered_changed(self, TRUE);
+
+    return;
+
+error:
+    g_signal_handlers_disconnect_by_func(priv->dbus_device_proxy, device_properties_changed, self);
+    g_clear_object(&priv->dbus_device_proxy);
+}
+
+gboolean
+nm_device_iwd_agent_query(NMDeviceIwd *self, GDBusMethodInvocation *invocation)
+{
+    NMDevice *                   device = NM_DEVICE(self);
+    NMDeviceIwdPrivate *         priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMDeviceState                state  = nm_device_get_state(device);
+    const char *                 setting_name;
+    const char *                 setting_key;
+    gboolean                     replied;
+    NMWifiAP *                   ap;
+    NMSecretAgentGetSecretsFlags get_secret_flags =
+        NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+    nm_auto_ref_string NMRefString *network_path = NULL;
+
+    if (!invocation) {
+        gs_unref_variant GVariant *value =
+            g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+        gboolean disconnect;
+
+        if (!priv->wifi_secrets_id && !priv->pending_agent_request)
+            return FALSE;
+
+        _LOGI(LOGD_WIFI, "IWD agent request is being cancelled");
+        wifi_secrets_cancel(self);
+
+        if (state == NM_DEVICE_STATE_NEED_AUTH)
+            nm_device_state_changed(device, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
+
+        /* The secrets request is being cancelled.  If we don't have an assumed
+         * connection than we've probably called Network.Connect and that method
+         * call's callback is going to handle the failure.  And if the state was
+         * not "disconnected" then let the state change handler process the
+         * failure.
+         */
+        if (!priv->assumed_ac)
+            return TRUE;
+
+        if (!nm_streq(get_variant_state(value), "disconnected"))
+            return TRUE;
+
+        disconnect = nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL);
+        cleanup_association_attempt(self, disconnect);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return TRUE;
+    }
+
+    if (state > NM_DEVICE_STATE_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
+        _LOGW(LOGD_WIFI, "Can't handle the IWD agent request in current device state");
+        return FALSE;
+    }
+
+    if (priv->wifi_secrets_id || priv->pending_agent_request) {
+        _LOGW(LOGD_WIFI, "There's already a pending agent request for this device");
+        return FALSE;
+    }
+
+    network_path = nm_ref_string_new(get_agent_request_network_path(invocation));
+    ap           = find_ap_by_supplicant_path(self, network_path);
+    if (!ap) {
+        _LOGW(LOGD_WIFI, "IWD Network object not found for the agent request");
+        return FALSE;
+    }
+
+    if (priv->assumed_ac) {
+        const char *ac_ap_path = nm_active_connection_get_specific_object(priv->assumed_ac);
+
+        if (!nm_streq(ac_ap_path, nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)))) {
+            _LOGW(LOGD_WIFI,
+                  "Dropping an existing assumed connection to create a new one based on the IWD "
+                  "agent request network parameter");
+
+            if (priv->current_ap)
+                nm_device_state_changed(device,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+
+            cleanup_association_attempt(self, FALSE);
+            priv->pending_agent_request = g_object_ref(invocation);
+            assume_connection(self, ap);
+            return TRUE;
+        }
+
+        if (state != NM_DEVICE_STATE_CONFIG) {
+            _LOGI(LOGD_WIFI, "IWD agent request deferred until in CONFIG");
+            priv->pending_agent_request = g_object_ref(invocation);
+            return TRUE;
+        }
+
+        /* Otherwise handle as usual */
+    } else if (!priv->current_ap) {
+        _LOGI(LOGD_WIFI, "IWD is asking for secrets without explicit connect request");
+
+        if (priv->iwd_autoconnect) {
+            priv->pending_agent_request = g_object_ref(invocation);
+            assume_connection(self, ap);
+            return TRUE;
+        }
+
+        send_disconnect(self);
+        return FALSE;
+    } else if (priv->current_ap) {
+        if (priv->current_ap != ap) {
+            _LOGW(LOGD_WIFI, "IWD agent request for a wrong network object");
+            cleanup_association_attempt(self, TRUE);
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+            return FALSE;
+        }
+
+        /* Otherwise handle as usual */
+    }
+
+    if (!try_reply_agent_request(self,
+                                 nm_device_get_applied_connection(device),
+                                 invocation,
+                                 &setting_name,
+                                 &setting_key,
+                                 &replied)) {
+        priv->secrets_failed = TRUE;
+        return FALSE;
+    }
+
+    if (replied)
+        return TRUE;
+
+    /* Normally require new secrets every time IWD asks for them.
+     * IWD only queries us if it has not saved the secrets (e.g. by policy)
+     * or a previous attempt has failed with current secrets so it wants
+     * a fresh set.  However if this is a new connection it may include
+     * all of the needed settings already so allow using these, too.
+     * Connection timestamp is set after activation or after first
+     * activation failure (to 0).
+     */
+    if (nm_settings_connection_get_timestamp(nm_device_get_settings_connection(device), NULL))
+        get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NO_SECRETS);
+    wifi_secrets_get_one(self, setting_name, get_secret_flags, setting_key, invocation);
+
+    return TRUE;
+}
+
+void
+nm_device_iwd_network_add_remove(NMDeviceIwd *self, GDBusProxy *network, bool add)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+    NMWifiAP *          ap   = NULL;
+    bool                recheck;
+    nm_auto_ref_string NMRefString *bss_path = NULL;
+
+    bss_path = nm_ref_string_new(g_dbus_proxy_get_object_path(network));
+    ap       = find_ap_by_supplicant_path(self, bss_path);
+
+    /* We could schedule an update_aps(self) idle call here but up to IWD 1.9
+     * when a hidden network connection is attempted, that network is initially
+     * only added as a Network object but not shown in GetOrderedNetworks()
+     * return values, and for some corner case scenarios it's beneficial to
+     * have that Network reflected in our ap list so that we don't attempt
+     * calling ConnectHiddenNetwork() on it, as that will fail in 1.9.  But we
+     * can skip recheck-available if we're currently scanning or in the middle
+     * of a GetOrderedNetworks() call as that will trigger the recheck too.
+     */
+    recheck = priv->enabled && !priv->scanning && !priv->networks_requested;
+
+    if (!add) {
+        if (ap) {
+            ap_add_remove(self, FALSE, ap, recheck);
+            priv->networks_changed |= !recheck;
+        }
+
+        return;
+    }
+
+    if (!ap) {
+        ap = ap_from_network(self,
+                             network,
+                             bss_path,
+                             nm_utils_get_monotonic_timestamp_msec(),
+                             -10000);
+        if (!ap)
+            return;
+
+        ap_add_remove(self, TRUE, ap, recheck);
+        g_object_unref(ap);
+        priv->networks_changed |= !recheck;
+        return;
+    }
+}
+
+static void
+autoconnect_changed(NMDevice *device, GParamSpec *pspec, NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv         = NM_DEVICE_IWD_GET_PRIVATE(self);
+    gs_unref_variant GVariant *value = NULL;
+
+    /* Note IWD normally remains in "disconnected" during a secret request
+     * and we don't want to interrupt it by calling Station.Disconnect().
+     */
+    if (!priv->dbus_station_proxy || !priv->iwd_autoconnect
+        || !nm_device_autoconnect_blocked_get(device, NM_DEVICE_AUTOCONNECT_BLOCKED_ALL)
+        || priv->wifi_secrets_id || priv->pending_agent_request)
+        return;
+
+    value = g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
+    if (!nm_streq(get_variant_state(value), "disconnected"))
+        return;
+
+    send_disconnect(self);
+}
+
+/*****************************************************************************/
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    nm_assert(NM_IS_DEVICE_IWD(device));
+
+    return "wifi";
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_iwd_init(NMDeviceIwd *self)
+{
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    c_list_init(&priv->aps_lst_head);
+
+    g_signal_connect(self, "notify::" NM_DEVICE_AUTOCONNECT, G_CALLBACK(autoconnect_changed), self);
+
+    /* Make sure the manager is running */
+    (void) nm_iwd_manager_get();
+}
+
+NMDevice *
+nm_device_iwd_new(const char *iface)
+{
+    return g_object_new(NM_TYPE_DEVICE_IWD,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "802.11 Wi-Fi",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_WIFI,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_WIFI,
+                        NM_DEVICE_RFKILL_TYPE,
+                        RFKILL_TYPE_WLAN,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceIwd *       self = NM_DEVICE_IWD(object);
+    NMDeviceIwdPrivate *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    g_signal_handlers_disconnect_by_func(self, autoconnect_changed, self);
+    nm_device_iwd_set_dbus_object(self, NULL);
+
+    G_OBJECT_CLASS(nm_device_iwd_parent_class)->dispose(object);
+
+    nm_assert(c_list_is_empty(&priv->aps_lst_head));
+}
+
+static void
+nm_device_iwd_class_init(NMDeviceIwdClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+
+    dbus_object_class->interface_infos =
+        NM_DBUS_INTERFACE_INFOS(&nm_interface_info_device_wireless);
+
+    device_class->connection_type_supported        = NM_SETTING_WIRELESS_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_WIRELESS_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI);
+
+    device_class->can_auto_connect            = can_auto_connect;
+    device_class->is_available                = is_available;
+    device_class->get_autoconnect_allowed     = get_autoconnect_allowed;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->get_enabled                 = get_enabled;
+    device_class->set_enabled                 = set_enabled;
+    device_class->get_type_description        = get_type_description;
+
+    device_class->act_stage1_prepare = act_stage1_prepare;
+    device_class->act_stage2_config  = act_stage2_config;
+    device_class->get_configured_mtu = get_configured_mtu;
+    device_class->deactivate         = deactivate;
+    device_class->deactivate_async   = deactivate_async;
+    device_class->can_reapply_change = can_reapply_change;
+
+    /* Stage 1 needed only for the set_current_ap() call.  Stage 2 is
+     * needed if we're assuming a connection still in the "connecting"
+     * state or on an agent request.
+     */
+    device_class->act_stage1_prepare_also_for_external_or_assume = TRUE;
+    device_class->act_stage2_config_also_for_external_or_assume  = TRUE;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_MODE] = g_param_spec_uint(NM_DEVICE_IWD_MODE,
+                                                  "",
+                                                  "",
+                                                  NM_802_11_MODE_UNKNOWN,
+                                                  NM_802_11_MODE_AP,
+                                                  NM_802_11_MODE_INFRA,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BITRATE] = g_param_spec_uint(NM_DEVICE_IWD_BITRATE,
+                                                     "",
+                                                     "",
+                                                     0,
+                                                     G_MAXUINT32,
+                                                     0,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ACCESS_POINTS] =
+        g_param_spec_boxed(NM_DEVICE_IWD_ACCESS_POINTS,
+                           "",
+                           "",
+                           G_TYPE_STRV,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ACTIVE_ACCESS_POINT] =
+        g_param_spec_string(NM_DEVICE_IWD_ACTIVE_ACCESS_POINT,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_IWD_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_WIFI_DEVICE_CAP_NONE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SCANNING] = g_param_spec_boolean(NM_DEVICE_IWD_SCANNING,
+                                                         "",
+                                                         "",
+                                                         FALSE,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LAST_SCAN] = g_param_spec_int64(NM_DEVICE_IWD_LAST_SCAN,
+                                                        "",
+                                                        "",
+                                                        -1,
+                                                        G_MAXINT64,
+                                                        -1,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wifi/nm-device-iwd.h b/src/core/devices/wifi/nm-device-iwd.h
new file mode 100644
index 00000000..ce94c9ea
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-iwd.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Intel Corporation
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_IWD_H__
+#define __NETWORKMANAGER_DEVICE_IWD_H__
+
+#include "devices/nm-device.h"
+#include "nm-wifi-ap.h"
+#include "nm-device-wifi.h"
+
+#define NM_TYPE_DEVICE_IWD (nm_device_iwd_get_type())
+#define NM_DEVICE_IWD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwd))
+#define NM_DEVICE_IWD_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass))
+#define NM_IS_DEVICE_IWD(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD))
+#define NM_IS_DEVICE_IWD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_IWD))
+#define NM_DEVICE_IWD_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_IWD, NMDeviceIwdClass))
+
+#define NM_DEVICE_IWD_MODE                NM_DEVICE_WIFI_MODE
+#define NM_DEVICE_IWD_BITRATE             NM_DEVICE_WIFI_BITRATE
+#define NM_DEVICE_IWD_ACCESS_POINTS       NM_DEVICE_WIFI_ACCESS_POINTS
+#define NM_DEVICE_IWD_ACTIVE_ACCESS_POINT NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT
+#define NM_DEVICE_IWD_CAPABILITIES        NM_DEVICE_WIFI_CAPABILITIES
+#define NM_DEVICE_IWD_SCANNING            NM_DEVICE_WIFI_SCANNING
+#define NM_DEVICE_IWD_LAST_SCAN           NM_DEVICE_WIFI_LAST_SCAN
+
+typedef struct _NMDeviceIwd      NMDeviceIwd;
+typedef struct _NMDeviceIwdClass NMDeviceIwdClass;
+
+GType nm_device_iwd_get_type(void);
+
+NMDevice *nm_device_iwd_new(const char *iface);
+
+void nm_device_iwd_set_dbus_object(NMDeviceIwd *device, GDBusObject *object);
+
+gboolean nm_device_iwd_agent_query(NMDeviceIwd *device, GDBusMethodInvocation *invocation);
+
+const CList *_nm_device_iwd_get_aps(NMDeviceIwd *self);
+
+void _nm_device_iwd_request_scan(NMDeviceIwd *          self,
+                                 GVariant *             options,
+                                 GDBusMethodInvocation *invocation);
+
+void nm_device_iwd_network_add_remove(NMDeviceIwd *device, GDBusProxy *network, bool add);
+
+#endif /* __NETWORKMANAGER_DEVICE_IWD_H__ */
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.c b/src/core/devices/wifi/nm-device-olpc-mesh.c
new file mode 100644
index 00000000..af83c4a3
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.c
@@ -0,0 +1,551 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Dan Williams <dcbw@redhat.com>
+ * Sjoerd Simons <sjoerd.simons@collabora.co.uk>
+ * Daniel Drake <dsd@laptop.org>
+ * Copyright (C) 2005 - 2014 Red Hat, Inc.
+ * Copyright (C) 2008 Collabora Ltd.
+ * Copyright (C) 2009 One Laptop per Child
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-olpc-mesh.h"
+
+#include <netinet/in.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+
+#include "devices/nm-device.h"
+#include "nm-device-wifi.h"
+#include "devices/nm-device-private.h"
+#include "nm-utils.h"
+#include "NetworkManagerUtils.h"
+#include "nm-act-request.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-olpc-mesh.h"
+#include "nm-manager.h"
+#include "platform/nm-platform.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceOlpcMesh
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceOlpcMesh, PROP_COMPANION, PROP_ACTIVE_CHANNEL, );
+
+typedef struct {
+    NMDevice * companion;
+    NMManager *manager;
+    bool       stage1_waiting : 1;
+} NMDeviceOlpcMeshPrivate;
+
+struct _NMDeviceOlpcMesh {
+    NMDevice                parent;
+    NMDeviceOlpcMeshPrivate _priv;
+};
+
+struct _NMDeviceOlpcMeshClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceOlpcMesh, nm_device_olpc_mesh, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_OLPC_MESH_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceOlpcMesh, NM_IS_DEVICE_OLPC_MESH, NMDevice)
+
+/*****************************************************************************/
+
+static gboolean
+get_autoconnect_allowed(NMDevice *device)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(device);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    /* We can't even connect if we don't have a companion yet. */
+    if (!priv->companion)
+        return FALSE;
+
+    /* We must not attempt to autoconnect when the companion is connected or
+     * connecting, * because we'd tear down its connection. */
+    if (nm_device_get_state(priv->companion) > NM_DEVICE_STATE_DISCONNECTED)
+        return FALSE;
+
+    return TRUE;
+}
+
+#define DEFAULT_SSID "olpc-mesh"
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMSettingOlpcMesh *s_mesh;
+
+    s_mesh = nm_connection_get_setting_olpc_mesh(connection);
+    if (!s_mesh) {
+        s_mesh = (NMSettingOlpcMesh *) nm_setting_olpc_mesh_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_mesh));
+    }
+
+    if (!nm_setting_olpc_mesh_get_ssid(s_mesh)) {
+        gs_unref_bytes GBytes *ssid = NULL;
+
+        ssid = g_bytes_new_static(DEFAULT_SSID, NM_STRLEN(DEFAULT_SSID));
+        g_object_set(G_OBJECT(s_mesh), NM_SETTING_OLPC_MESH_SSID, ssid, NULL);
+    }
+
+    if (!nm_setting_olpc_mesh_get_dhcp_anycast_address(s_mesh)) {
+        const char *anycast = "c0:27:c0:27:c0:27";
+
+        g_object_set(G_OBJECT(s_mesh), NM_SETTING_OLPC_MESH_DHCP_ANYCAST_ADDRESS, anycast, NULL);
+    }
+
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_OLPC_MESH_SETTING_NAME,
+                              existing_connections,
+                              NULL,
+                              _("Mesh"),
+                              NULL,
+                              NULL,
+                              FALSE); /* No IPv6 by default */
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(device);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    /* disconnect companion device, if it is connected */
+    if (nm_device_get_act_request(NM_DEVICE(priv->companion))) {
+        _LOGI(LOGD_OLPC, "disconnecting companion device %s", nm_device_get_iface(priv->companion));
+        /* FIXME: VPN stuff here is a bug; but we can't really change API now... */
+        nm_device_state_changed(NM_DEVICE(priv->companion),
+                                NM_DEVICE_STATE_DISCONNECTED,
+                                NM_DEVICE_STATE_REASON_USER_REQUESTED);
+        _LOGI(LOGD_OLPC, "companion %s disconnected", nm_device_get_iface(priv->companion));
+    }
+
+    /* wait with continuing configuration until the companion device is done scanning */
+    if (nm_device_wifi_get_scanning(NM_DEVICE_WIFI(priv->companion))) {
+        priv->stage1_waiting = TRUE;
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    priv->stage1_waiting = FALSE;
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static gboolean
+_mesh_set_channel(NMDeviceOlpcMesh *self, guint32 channel)
+{
+    NMPlatform *platform;
+    int         ifindex = nm_device_get_ifindex(NM_DEVICE(self));
+    guint32     old_channel;
+
+    platform    = nm_device_get_platform(NM_DEVICE(self));
+    old_channel = nm_platform_mesh_get_channel(platform, ifindex);
+
+    if (channel == 0)
+        channel = old_channel;
+
+    /* We want to call this even if the channel number is the same,
+     * because that actually starts the mesh with the configured mesh ID. */
+    if (!nm_platform_mesh_set_channel(platform, ifindex, channel))
+        return FALSE;
+
+    if (old_channel != channel)
+        _notify(self, PROP_ACTIVE_CHANNEL);
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceOlpcMesh * self = NM_DEVICE_OLPC_MESH(device);
+    NMSettingOlpcMesh *s_mesh;
+    GBytes *           ssid;
+    const char *       anycast_addr;
+    gboolean           success;
+
+    s_mesh = nm_device_get_applied_setting(device, NM_TYPE_SETTING_OLPC_MESH);
+    g_return_val_if_fail(s_mesh, NM_ACT_STAGE_RETURN_FAILURE);
+
+    ssid = nm_setting_olpc_mesh_get_ssid(s_mesh);
+
+    nm_device_take_down(NM_DEVICE(self), TRUE);
+    success = nm_platform_mesh_set_ssid(nm_device_get_platform(device),
+                                        nm_device_get_ifindex(device),
+                                        g_bytes_get_data(ssid, NULL),
+                                        g_bytes_get_size(ssid));
+    nm_device_bring_up(NM_DEVICE(self), TRUE, NULL);
+    if (!success) {
+        _LOGW(LOGD_WIFI, "Unable to set the mesh ID");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    anycast_addr = nm_setting_olpc_mesh_get_dhcp_anycast_address(s_mesh);
+    nm_device_set_dhcp_anycast_address(device, anycast_addr);
+
+    if (!_mesh_set_channel(self, nm_setting_olpc_mesh_get_channel(s_mesh))) {
+        _LOGW(LOGD_WIFI, "Unable to set the mesh channel");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(device);
+
+    if (!NM_DEVICE_OLPC_MESH_GET_PRIVATE(self)->companion) {
+        _LOGD(LOGD_WIFI, "not available because companion not found");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+companion_cleanup(NMDeviceOlpcMesh *self)
+{
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    if (priv->companion) {
+        nm_device_wifi_scanning_prohibited_track(NM_DEVICE_WIFI(priv->companion), self, FALSE);
+        g_signal_handlers_disconnect_by_data(priv->companion, self);
+        g_clear_object(&priv->companion);
+    }
+    _notify(self, PROP_COMPANION);
+}
+
+static void
+companion_notify_cb(NMDeviceWifi *companion, GParamSpec *pspec, gpointer user_data)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(user_data);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    nm_assert(NM_IS_DEVICE_WIFI(companion));
+    nm_assert(priv->companion == (gpointer) companion);
+
+    if (!priv->stage1_waiting)
+        return;
+
+    if (!nm_device_wifi_get_scanning(NM_DEVICE_WIFI(companion))) {
+        priv->stage1_waiting = FALSE;
+        nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+    }
+}
+
+/* disconnect from mesh if someone starts using the companion */
+static void
+companion_state_changed_cb(NMDeviceWifi *      companion,
+                           NMDeviceState       state,
+                           NMDeviceState       old_state,
+                           NMDeviceStateReason reason,
+                           gpointer            user_data)
+{
+    NMDeviceOlpcMesh *self       = NM_DEVICE_OLPC_MESH(user_data);
+    NMDeviceState     self_state = nm_device_get_state(NM_DEVICE(self));
+
+    if (old_state > NM_DEVICE_STATE_DISCONNECTED && state <= NM_DEVICE_STATE_DISCONNECTED) {
+        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+    }
+
+    if (self_state < NM_DEVICE_STATE_PREPARE || self_state > NM_DEVICE_STATE_ACTIVATED
+        || state < NM_DEVICE_STATE_PREPARE || state > NM_DEVICE_STATE_ACTIVATED)
+        return;
+
+    _LOGD(LOGD_OLPC, "disconnecting mesh due to companion connectivity");
+    /* FIXME: VPN stuff here is a bug; but we can't really change API now... */
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_DISCONNECTED,
+                            NM_DEVICE_STATE_REASON_USER_REQUESTED);
+}
+
+static gboolean
+companion_autoconnect_allowed_cb(NMDeviceWifi *companion, gpointer user_data)
+{
+    NMDeviceOlpcMesh *self  = NM_DEVICE_OLPC_MESH(user_data);
+    NMDeviceState     state = nm_device_get_state(NM_DEVICE(self));
+
+    /* Don't allow the companion to autoconnect while a mesh connection is
+     * active */
+    return (state < NM_DEVICE_STATE_PREPARE) || (state > NM_DEVICE_STATE_ACTIVATED);
+}
+
+static gboolean
+check_companion(NMDeviceOlpcMesh *self, NMDevice *other)
+{
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+    const char *             my_addr, *their_addr;
+
+    if (!NM_IS_DEVICE_WIFI(other))
+        return FALSE;
+
+    my_addr    = nm_device_get_hw_address(NM_DEVICE(self));
+    their_addr = nm_device_get_hw_address(other);
+    if (!nm_utils_hwaddr_matches(my_addr, -1, their_addr, -1))
+        return FALSE;
+
+    nm_assert(priv->companion == NULL);
+    priv->companion = g_object_ref(other);
+
+    _LOGI(LOGD_OLPC, "found companion Wi-Fi device %s", nm_device_get_iface(other));
+
+    g_signal_connect(G_OBJECT(other),
+                     NM_DEVICE_STATE_CHANGED,
+                     G_CALLBACK(companion_state_changed_cb),
+                     self);
+
+    g_signal_connect(G_OBJECT(other),
+                     "notify::" NM_DEVICE_WIFI_SCANNING,
+                     G_CALLBACK(companion_notify_cb),
+                     self);
+
+    g_signal_connect(G_OBJECT(other),
+                     NM_DEVICE_AUTOCONNECT_ALLOWED,
+                     G_CALLBACK(companion_autoconnect_allowed_cb),
+                     self);
+
+    _notify(self, PROP_COMPANION);
+
+    return TRUE;
+}
+
+static void
+device_added_cb(NMManager *manager, NMDevice *other, gpointer user_data)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(user_data);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    if (!priv->companion && check_companion(self, other)) {
+        nm_device_queue_recheck_available(NM_DEVICE(self),
+                                          NM_DEVICE_STATE_REASON_NONE,
+                                          NM_DEVICE_STATE_REASON_NONE);
+        nm_device_remove_pending_action(NM_DEVICE(self),
+                                        NM_PENDING_ACTION_WAITING_FOR_COMPANION,
+                                        FALSE);
+    }
+}
+
+static void
+device_removed_cb(NMManager *manager, NMDevice *other, gpointer user_data)
+{
+    NMDeviceOlpcMesh *self = NM_DEVICE_OLPC_MESH(user_data);
+
+    if (other == NM_DEVICE_OLPC_MESH_GET_PRIVATE(self)->companion)
+        companion_cleanup(self);
+}
+
+static void
+find_companion(NMDeviceOlpcMesh *self)
+{
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+    const CList *            tmp_lst;
+    NMDevice *               candidate;
+
+    if (priv->companion)
+        return;
+
+    nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WAITING_FOR_COMPANION, TRUE);
+
+    /* Try to find the companion if it's already known to the NMManager */
+    nm_manager_for_each_device (priv->manager, candidate, tmp_lst) {
+        if (check_companion(self, candidate)) {
+            nm_device_queue_recheck_available(NM_DEVICE(self),
+                                              NM_DEVICE_STATE_REASON_NONE,
+                                              NM_DEVICE_STATE_REASON_NONE);
+            nm_device_remove_pending_action(NM_DEVICE(self),
+                                            NM_PENDING_ACTION_WAITING_FOR_COMPANION,
+                                            TRUE);
+            break;
+        }
+    }
+}
+
+static void
+state_changed(NMDevice *          device,
+              NMDeviceState       new_state,
+              NMDeviceState       old_state,
+              NMDeviceStateReason reason)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(device);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    if (new_state == NM_DEVICE_STATE_UNAVAILABLE)
+        find_companion(self);
+
+    if (priv->companion) {
+        gboolean temporarily_prohibited = FALSE;
+
+        if (new_state >= NM_DEVICE_STATE_PREPARE && new_state <= NM_DEVICE_STATE_IP_CONFIG) {
+            /* Don't allow the companion to scan while configuring the mesh interface */
+            temporarily_prohibited = TRUE;
+        }
+        nm_device_wifi_scanning_prohibited_track(NM_DEVICE_WIFI(priv->companion),
+                                                 self,
+                                                 temporarily_prohibited);
+    }
+}
+
+static guint32
+get_dhcp_timeout_for_device(NMDevice *device, int addr_family)
+{
+    /* shorter timeout for mesh connectivity */
+    return 20;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceOlpcMesh *       self   = NM_DEVICE_OLPC_MESH(object);
+    NMDevice *               device = NM_DEVICE(self);
+    NMDeviceOlpcMeshPrivate *priv   = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_COMPANION:
+        nm_dbus_utils_g_value_set_object_path(value, priv->companion);
+        break;
+    case PROP_ACTIVE_CHANNEL:
+        g_value_set_uint(value,
+                         nm_platform_mesh_get_channel(nm_device_get_platform(device),
+                                                      nm_device_get_ifindex(device)));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_olpc_mesh_init(NMDeviceOlpcMesh *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(object);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_device_olpc_mesh_parent_class)->constructed(object);
+
+    priv->manager = g_object_ref(NM_MANAGER_GET);
+
+    g_signal_connect(priv->manager, NM_MANAGER_DEVICE_ADDED, G_CALLBACK(device_added_cb), self);
+    g_signal_connect(priv->manager, NM_MANAGER_DEVICE_REMOVED, G_CALLBACK(device_removed_cb), self);
+}
+
+NMDevice *
+nm_device_olpc_mesh_new(const char *iface)
+{
+    return g_object_new(NM_TYPE_DEVICE_OLPC_MESH,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "802.11 OLPC Mesh",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_OLPC_MESH,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_OLPC_MESH,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceOlpcMesh *       self = NM_DEVICE_OLPC_MESH(object);
+    NMDeviceOlpcMeshPrivate *priv = NM_DEVICE_OLPC_MESH_GET_PRIVATE(self);
+
+    companion_cleanup(self);
+
+    if (priv->manager) {
+        g_signal_handlers_disconnect_by_func(priv->manager, G_CALLBACK(device_added_cb), self);
+        g_signal_handlers_disconnect_by_func(priv->manager, G_CALLBACK(device_removed_cb), self);
+        g_clear_object(&priv->manager);
+    }
+
+    G_OBJECT_CLASS(nm_device_olpc_mesh_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_olpc_mesh = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_OLPC_MESH,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Companion",
+                                                             "o",
+                                                             NM_DEVICE_OLPC_MESH_COMPANION),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L(
+                "ActiveChannel",
+                "u",
+                NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_olpc_mesh_class_init(NMDeviceOlpcMeshClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_olpc_mesh);
+
+    device_class->connection_type_supported        = NM_SETTING_OLPC_MESH_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_OLPC_MESH_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_OLPC_MESH);
+
+    device_class->get_autoconnect_allowed     = get_autoconnect_allowed;
+    device_class->complete_connection         = complete_connection;
+    device_class->is_available                = is_available;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->state_changed               = state_changed;
+    device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device;
+
+    obj_properties[PROP_COMPANION] = g_param_spec_string(NM_DEVICE_OLPC_MESH_COMPANION,
+                                                         "",
+                                                         "",
+                                                         NULL,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ACTIVE_CHANNEL] =
+        g_param_spec_uint(NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          0,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.h b/src/core/devices/wifi/nm-device-olpc-mesh.h
new file mode 100644
index 00000000..79b7fd5d
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Dan Williams <dcbw@redhat.com>
+ * Sjoerd Simons <sjoerd.simons@collabora.co.uk>
+ * Daniel Drake <dsd@laptop.org>
+ * Copyright (C) 2005 Red Hat, Inc.
+ * Copyright (C) 2008 Collabora Ltd.
+ * Copyright (C) 2009 One Laptop per Child
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_OLPC_MESH_H__
+#define __NETWORKMANAGER_DEVICE_OLPC_MESH_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_OLPC_MESH (nm_device_olpc_mesh_get_type())
+#define NM_DEVICE_OLPC_MESH(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMesh))
+#define NM_DEVICE_OLPC_MESH_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass))
+#define NM_IS_DEVICE_OLPC_MESH(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_OLPC_MESH))
+#define NM_IS_DEVICE_OLPC_MESH_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_OLPC_MESH))
+#define NM_DEVICE_OLPC_MESH_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_OLPC_MESH, NMDeviceOlpcMeshClass))
+
+#define NM_DEVICE_OLPC_MESH_COMPANION      "companion"
+#define NM_DEVICE_OLPC_MESH_BITRATE        "bitrate"
+#define NM_DEVICE_OLPC_MESH_ACTIVE_CHANNEL "active-channel"
+
+typedef struct _NMDeviceOlpcMesh      NMDeviceOlpcMesh;
+typedef struct _NMDeviceOlpcMeshClass NMDeviceOlpcMeshClass;
+
+GType nm_device_olpc_mesh_get_type(void);
+
+NMDevice *nm_device_olpc_mesh_new(const char *iface);
+
+#endif /* __NETWORKMANAGER_DEVICE_OLPC_MESH_H__ */
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
new file mode 100644
index 00000000..fb987600
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -0,0 +1,1280 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-wifi-p2p.h"
+
+#include <sys/socket.h>
+
+#include "supplicant/nm-supplicant-manager.h"
+#include "supplicant/nm-supplicant-interface.h"
+
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "nm-act-request.h"
+#include "nm-core-internal.h"
+#include "nm-glib-aux/nm-ref-string.h"
+#include "nm-ip4-config.h"
+#include "nm-manager.h"
+#include "nm-manager.h"
+#include "nm-setting-wifi-p2p.h"
+#include "nm-utils.h"
+#include "nm-wifi-p2p-peer.h"
+#include "platform/nm-platform.h"
+#include "platform/nmp-object.h"
+#include "settings/nm-settings.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceWifiP2P
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWifiP2P, PROP_PEERS, );
+
+typedef struct {
+    NMSupplicantManager *sup_mgr;
+
+    /* NOTE: In theory management and group ifaces could be identical. However,
+     * in practice, this cannot happen currently as NMDeviceWifiP2P is only
+     * created for existing non-P2P interfaces.
+     * (i.e. a single standalone P2P interface is not supported at this point)
+     */
+    NMSupplicantInterface *mgmt_iface;
+    NMSupplicantInterface *group_iface;
+
+    CList peers_lst_head;
+
+    guint find_peer_timeout_id;
+    guint sup_timeout_id;
+    guint peer_dump_id;
+    guint peer_missing_id;
+
+    bool is_waiting_for_supplicant : 1;
+} NMDeviceWifiP2PPrivate;
+
+struct _NMDeviceWifiP2P {
+    NMDevice               parent;
+    NMDeviceWifiP2PPrivate _priv;
+};
+
+struct _NMDeviceWifiP2PClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceWifiP2P, nm_device_wifi_p2p, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_WIFI_P2P_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceWifiP2P, NM_IS_DEVICE_WIFI_P2P, NMDevice)
+
+/*****************************************************************************/
+
+static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p;
+static const GDBusSignalInfo             nm_signal_info_wifi_p2p_peer_added;
+static const GDBusSignalInfo             nm_signal_info_wifi_p2p_peer_removed;
+
+static void supplicant_group_interface_release(NMDeviceWifiP2P *self);
+static void supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting);
+
+/*****************************************************************************/
+
+static void
+_peer_dump(NMDeviceWifiP2P *    self,
+           NMLogLevel           log_level,
+           const NMWifiP2PPeer *peer,
+           const char *         prefix,
+           gint32               now_s)
+{
+    char buf[1024];
+
+    _NMLOG(log_level,
+           LOGD_WIFI_SCAN,
+           "wifi-peer: %-7s %s",
+           prefix,
+           nm_wifi_p2p_peer_to_string(peer, buf, sizeof(buf), now_s));
+}
+
+static gboolean
+peer_list_dump(gpointer user_data)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(user_data);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    priv->peer_dump_id = 0;
+
+    if (_LOGD_ENABLED(LOGD_WIFI_SCAN)) {
+        NMWifiP2PPeer *peer;
+        gint32         now_s = nm_utils_get_monotonic_timestamp_sec();
+
+        _LOGD(LOGD_WIFI_SCAN, "P2P Peers: [now:%u]", now_s);
+        c_list_for_each_entry (peer, &priv->peers_lst_head, peers_lst)
+            _peer_dump(self, LOGL_DEBUG, peer, "dump", now_s);
+    }
+    return G_SOURCE_REMOVE;
+}
+
+static void
+schedule_peer_list_dump(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (!priv->peer_dump_id && _LOGD_ENABLED(LOGD_WIFI_SCAN))
+        priv->peer_dump_id = g_timeout_add_seconds(1, peer_list_dump, self);
+}
+
+/*****************************************************************************/
+
+static void
+_set_is_waiting_for_supplicant(NMDeviceWifiP2P *self, gboolean is_waiting)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (priv->is_waiting_for_supplicant == (!!is_waiting))
+        return;
+
+    priv->is_waiting_for_supplicant = is_waiting;
+
+    if (is_waiting)
+        nm_device_add_pending_action(NM_DEVICE(self),
+                                     NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
+                                     TRUE);
+    else
+        nm_device_remove_pending_action(NM_DEVICE(self),
+                                        NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
+                                        TRUE);
+}
+
+/*****************************************************************************/
+
+static gboolean
+check_connection_peer_joined(NMDeviceWifiP2P *device)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(device);
+    NMConnection *          conn = nm_device_get_applied_connection(NM_DEVICE(device));
+    NMWifiP2PPeer *         peer;
+    const char *            group;
+    const char *const *     groups;
+
+    if (!conn || !priv->group_iface)
+        return FALSE;
+
+    /* Comparing the object path found on the group_iface with the peers
+     * found on the mgmt_iface is legal. */
+    group = nm_supplicant_interface_get_p2p_group_path(priv->group_iface);
+    if (!group)
+        return FALSE;
+
+    /* NOTE: We currently only support connections to a specific peer */
+    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, conn);
+    if (!peer)
+        return FALSE;
+
+    groups = nm_wifi_p2p_peer_get_groups(peer);
+    if (!groups || !g_strv_contains(groups, group))
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+disconnect_on_connection_peer_missing_cb(gpointer user_data)
+{
+    NMDevice *              device = NM_DEVICE(user_data);
+    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    _LOGW(LOGD_WIFI, "Peer requested in connection is missing for too long, failing connection.");
+
+    priv->peer_missing_id = 0;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
+    return FALSE;
+}
+
+static void
+update_disconnect_on_connection_peer_missing(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMDeviceState           state;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (state < NM_DEVICE_STATE_IP_CONFIG || state > NM_DEVICE_STATE_ACTIVATED) {
+        nm_clear_g_source(&priv->peer_missing_id);
+        return;
+    }
+
+    if (check_connection_peer_joined(self)) {
+        if (nm_clear_g_source(&priv->peer_missing_id))
+            _LOGD(LOGD_WIFI, "Peer requested in connection is joined, removing timeout");
+        return;
+    }
+
+    if (priv->peer_missing_id == 0) {
+        _LOGD(LOGD_WIFI, "Peer requested in connection is missing, adding timeout");
+        priv->peer_missing_id =
+            g_timeout_add_seconds(5, disconnect_on_connection_peer_missing_cb, self);
+    }
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceWifiP2P *          self = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *   priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMSupplicantInterfaceState supplicant_state;
+
+    if (!priv->mgmt_iface)
+        return FALSE;
+
+    supplicant_state = nm_supplicant_interface_get_state(priv->mgmt_iface);
+    return nm_supplicant_interface_state_is_operational(supplicant_state);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    if (!NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    /* TODO: Allow limitting the interface using the HW-address? */
+
+    /* We don't need to check anything else here. The P2P device will only
+     * exists if we are able to establish a P2P connection, and there should
+     * be no further restrictions necessary.
+     */
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceWifiP2P * self         = NM_DEVICE_WIFI_P2P(device);
+    gs_free char *    setting_name = NULL;
+    NMSettingWifiP2P *s_wifi_p2p;
+    NMWifiP2PPeer *   peer;
+    const char *      setting_peer;
+
+    s_wifi_p2p =
+        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
+
+    if (!specific_object) {
+        /* If not given a specific object, we need at minimum a peer address */
+        if (!s_wifi_p2p) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "A '%s' setting is required if no Peer path was given",
+                        NM_SETTING_WIFI_P2P_SETTING_NAME);
+            return FALSE;
+        }
+
+        setting_peer = nm_setting_wifi_p2p_get_peer(s_wifi_p2p);
+        if (!setting_peer) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "A '%s' setting with a valid Peer is required if no Peer path was given",
+                        NM_SETTING_WIFI_P2P_SETTING_NAME);
+            return FALSE;
+        }
+
+    } else {
+        peer = nm_wifi_p2p_peer_lookup_for_device(NM_DEVICE(self), specific_object);
+        if (!peer) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND,
+                        "The P2P peer %s is unknown",
+                        specific_object);
+            return FALSE;
+        }
+
+        setting_peer = nm_wifi_p2p_peer_get_address(peer);
+        g_return_val_if_fail(setting_peer, FALSE);
+    }
+
+    /* Add a Wi-Fi P2P setting if one doesn't exist yet */
+    if (!s_wifi_p2p) {
+        s_wifi_p2p = NM_SETTING_WIFI_P2P(nm_setting_wifi_p2p_new());
+        nm_connection_add_setting(connection, NM_SETTING(s_wifi_p2p));
+    }
+
+    g_object_set(G_OBJECT(s_wifi_p2p), NM_SETTING_WIFI_P2P_PEER, setting_peer, NULL);
+
+    setting_name = g_strdup_printf("Wi-Fi P2P Peer %s", setting_peer);
+    nm_utils_complete_generic(nm_device_get_platform(device),
+                              connection,
+                              NM_SETTING_WIFI_P2P_SETTING_NAME,
+                              existing_connections,
+                              setting_name,
+                              setting_name,
+                              NULL,
+                              NULL,
+                              TRUE);
+
+    return TRUE;
+}
+
+/*
+ * supplicant_find_timeout_cb
+ *
+ * Called when the supplicant has been unable to find the peer we want to connect to.
+ */
+static gboolean
+supplicant_find_timeout_cb(gpointer user_data)
+{
+    NMDevice *              device = NM_DEVICE(user_data);
+    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(user_data);
+    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    priv->find_peer_timeout_id = 0;
+
+    nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);
+
+    if (nm_device_is_activating(device)) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi-p2p) could not find peer, failing activation");
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMConnection *          connection;
+    NMSettingWifiP2P *      s_wifi_p2p;
+    NMWifiP2PPeer *         peer;
+
+    if (!priv->mgmt_iface) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_wifi_p2p =
+        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
+    g_return_val_if_fail(s_wifi_p2p, NM_ACT_STAGE_RETURN_FAILURE);
+
+    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
+    if (!peer) {
+        /* Set up a timeout on the find attempt and run a find for the same period of time */
+        if (priv->find_peer_timeout_id == 0) {
+            priv->find_peer_timeout_id =
+                g_timeout_add_seconds(10, supplicant_find_timeout_cb, self);
+
+            nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, 10);
+        }
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+/*
+ * supplicant_connection_timeout_cb
+ *
+ * Called when the supplicant has been unable to connect to a peer
+ * within a specified period of time.
+ */
+static gboolean
+supplicant_connection_timeout_cb(gpointer user_data)
+{
+    NMDevice *              device = NM_DEVICE(user_data);
+    NMDeviceWifiP2P *       self   = NM_DEVICE_WIFI_P2P(user_data);
+    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    priv->sup_timeout_id = 0;
+
+    nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);
+
+    if (nm_device_is_activating(device)) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi-p2p) connecting took too long, failing activation");
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMConnection *          connection;
+    NMSettingWifiP2P *      s_wifi_p2p;
+    NMWifiP2PPeer *         peer;
+    GBytes *                wfd_ies;
+
+    if (nm_clear_g_source(&priv->find_peer_timeout_id))
+        nm_assert_not_reached();
+
+    if (!priv->mgmt_iface) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    connection = nm_device_get_applied_connection(device);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+    nm_assert(
+        NM_IS_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P)));
+
+    /* The prepare stage ensures that the peer has been found */
+    peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
+    if (!peer) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PEER_NOT_FOUND);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    /* Set the WFD IEs before trying to establish the connection. */
+    s_wifi_p2p =
+        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
+    wfd_ies = nm_setting_wifi_p2p_get_wfd_ies(s_wifi_p2p);
+    nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, wfd_ies);
+
+    /* TODO: Grab secrets if we don't have them yet! */
+
+    /* TODO: Fix "pbc" being hardcoded here! */
+    nm_supplicant_interface_p2p_connect(priv->mgmt_iface,
+                                        nm_wifi_p2p_peer_get_supplicant_path(peer),
+                                        "pbc",
+                                        NULL);
+
+    /* Set up a timeout on the connect attempt */
+    if (priv->sup_timeout_id == 0) {
+        priv->sup_timeout_id = g_timeout_add_seconds(45, supplicant_connection_timeout_cb, self);
+    }
+
+    /* We'll get stage3 started when the P2P group has been started */
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static void
+emit_signal_p2p_peer_add_remove(NMDeviceWifiP2P *device,
+                                NMWifiP2PPeer *  peer,
+                                gboolean         is_added /* or else is_removed */)
+{
+    nm_dbus_object_emit_signal(NM_DBUS_OBJECT(device),
+                               &interface_info_device_wifi_p2p,
+                               is_added ? &nm_signal_info_wifi_p2p_peer_added
+                                        : &nm_signal_info_wifi_p2p_peer_removed,
+                               "(o)",
+                               nm_dbus_object_get_path(NM_DBUS_OBJECT(peer)));
+}
+
+static void
+peer_add_remove(NMDeviceWifiP2P *self,
+                gboolean         is_adding, /* or else removing */
+                NMWifiP2PPeer *  peer,
+                gboolean         recheck_available_connections)
+{
+    NMDevice *              device = NM_DEVICE(self);
+    NMDeviceWifiP2PPrivate *priv   = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (is_adding) {
+        g_object_ref(peer);
+        peer->wifi_device = device;
+        c_list_link_tail(&priv->peers_lst_head, &peer->peers_lst);
+        nm_dbus_object_export(NM_DBUS_OBJECT(peer));
+        _peer_dump(self, LOGL_DEBUG, peer, "added", 0);
+
+        emit_signal_p2p_peer_add_remove(self, peer, TRUE);
+    } else {
+        peer->wifi_device = NULL;
+        c_list_unlink(&peer->peers_lst);
+        _peer_dump(self, LOGL_DEBUG, peer, "removed", 0);
+    }
+
+    _notify(self, PROP_PEERS);
+
+    if (!is_adding) {
+        emit_signal_p2p_peer_add_remove(self, peer, FALSE);
+        nm_dbus_object_clear_and_unexport(&peer);
+    }
+
+    if (is_adding) {
+        /* If we are in prepare state, then we are currently runnign a find
+         * to search for the requested peer. */
+        if (priv->find_peer_timeout_id != 0) {
+            NMConnection *connection;
+
+            nm_assert(nm_device_get_state(device) == NM_DEVICE_STATE_PREPARE);
+
+            connection = nm_device_get_applied_connection(device);
+            nm_assert(NM_IS_CONNECTION(connection));
+
+            peer = nm_wifi_p2p_peers_find_first_compatible(&priv->peers_lst_head, connection);
+            if (peer) {
+                /* A peer for the connection was found, cancel the timeout and go to configure state. */
+                nm_clear_g_source(&priv->find_peer_timeout_id);
+                nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+            }
+        }
+
+        /* TODO: We may want to re-check auto-activation here, otherwise it will never work. */
+    }
+
+    update_disconnect_on_connection_peer_missing(self);
+}
+
+static void
+remove_all_peers(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMWifiP2PPeer *         peer;
+
+    if (c_list_is_empty(&priv->peers_lst_head))
+        return;
+
+    while ((peer = c_list_first_entry(&priv->peers_lst_head, NMWifiP2PPeer, peers_lst)))
+        peer_add_remove(self, FALSE, peer, FALSE);
+
+    nm_device_recheck_available_connections(NM_DEVICE(self));
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    gboolean      indicate_addressing_running;
+    NMConnection *connection;
+    const char *  method;
+
+    connection = nm_device_get_applied_connection(device);
+
+    method = nm_utils_get_ip_config_method(connection, addr_family);
+
+    if (addr_family == AF_INET)
+        indicate_addressing_running = NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+    else {
+        indicate_addressing_running = NM_IN_STRSET(method,
+                                                   NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                                                   NM_SETTING_IP6_CONFIG_METHOD_DHCP);
+    }
+
+    if (indicate_addressing_running)
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     nm_device_get_ip_ifindex(device),
+                                                     TRUE);
+
+    return NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceWifiP2P *       self    = NM_DEVICE_WIFI_P2P(device);
+    int                     ifindex = nm_device_get_ip_ifindex(device);
+    NMDeviceWifiP2PPrivate *priv    = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->find_peer_timeout_id);
+    nm_clear_g_source(&priv->sup_timeout_id);
+    nm_clear_g_source(&priv->peer_missing_id);
+
+    if (priv->mgmt_iface)
+        nm_supplicant_interface_p2p_cancel_connect(priv->mgmt_iface);
+
+    if (priv->group_iface)
+        nm_supplicant_interface_p2p_disconnect(priv->group_iface);
+
+    /* Clear any critical protocol notification in the Wi-Fi stack */
+    if (ifindex > 0)
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     ifindex,
+                                                     FALSE);
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
+    return 0;
+}
+
+static const char *
+get_auto_ip_config_method(NMDevice *device, int addr_family)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    /* Override the AUTO method to mean shared if we are group owner. */
+    if (priv->group_iface && nm_supplicant_interface_get_p2p_group_owner(priv->group_iface)) {
+        if (addr_family == AF_INET)
+            return NM_SETTING_IP4_CONFIG_METHOD_SHARED;
+
+        if (addr_family == AF_INET6)
+            return NM_SETTING_IP6_CONFIG_METHOD_SHARED;
+    }
+
+    return NULL;
+}
+
+static gboolean
+unmanaged_on_quit(NMDevice *self)
+{
+    return TRUE;
+}
+
+static void
+supplicant_iface_state_cb(NMSupplicantInterface *iface,
+                          int                    new_state_i,
+                          int                    old_state_i,
+                          int                    disconnect_reason,
+                          gpointer               user_data)
+{
+    NMDeviceWifiP2P *          self      = NM_DEVICE_WIFI_P2P(user_data);
+    NMDevice *                 device    = NM_DEVICE(self);
+    NMSupplicantInterfaceState new_state = new_state_i;
+    NMSupplicantInterfaceState old_state = old_state_i;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "supplicant management interface state: %s -> %s",
+          nm_supplicant_interface_state_to_string(old_state),
+          nm_supplicant_interface_state_to_string(new_state));
+
+    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
+        supplicant_interfaces_release(self, TRUE);
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        _LOGD(LOGD_WIFI, "supplicant ready");
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        _set_is_waiting_for_supplicant(self, FALSE);
+    }
+}
+
+static void
+supplicant_iface_peer_changed_cb(NMSupplicantInterface *iface,
+                                 NMSupplicantPeerInfo * peer_info,
+                                 gboolean               is_present,
+                                 NMDeviceWifiP2P *      self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    NMWifiP2PPeer *         found_peer;
+
+    found_peer =
+        nm_wifi_p2p_peers_find_by_supplicant_path(&priv->peers_lst_head, peer_info->peer_path->str);
+
+    if (!is_present) {
+        if (!found_peer)
+            return;
+
+        peer_add_remove(self, FALSE, found_peer, TRUE);
+        goto out;
+    }
+
+    if (found_peer) {
+        if (!nm_wifi_p2p_peer_update_from_properties(found_peer, peer_info))
+            return;
+
+        update_disconnect_on_connection_peer_missing(self);
+        _peer_dump(self, LOGL_DEBUG, found_peer, "updated", 0);
+    } else {
+        gs_unref_object NMWifiP2PPeer *peer = NULL;
+
+        peer = nm_wifi_p2p_peer_new_from_properties(peer_info);
+        peer_add_remove(self, TRUE, peer, TRUE);
+    }
+
+out:
+    schedule_peer_list_dump(self);
+}
+
+static void
+check_group_iface_ready(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (!priv->group_iface)
+        return;
+
+    if (!nm_supplicant_interface_state_is_operational(
+            nm_supplicant_interface_get_state(priv->group_iface)))
+        return;
+
+    if (!nm_supplicant_interface_get_p2p_group_joined(priv->group_iface))
+        return;
+
+    nm_clear_g_source(&priv->sup_timeout_id);
+    update_disconnect_on_connection_peer_missing(self);
+
+    nm_device_activate_schedule_stage3_ip_config_start(NM_DEVICE(self));
+}
+
+static void
+supplicant_group_iface_is_ready(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    _LOGD(LOGD_WIFI, "P2P Group supplicant ready");
+
+    if (!nm_device_set_ip_iface(NM_DEVICE(self),
+                                nm_supplicant_interface_get_ifname(priv->group_iface))) {
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        return;
+    }
+
+    _set_is_waiting_for_supplicant(self, FALSE);
+    check_group_iface_ready(self);
+}
+
+static void
+supplicant_group_iface_state_cb(NMSupplicantInterface *iface,
+                                int                    new_state_i,
+                                int                    old_state_i,
+                                int                    disconnect_reason,
+                                gpointer               user_data)
+{
+    NMDeviceWifiP2P *          self      = NM_DEVICE_WIFI_P2P(user_data);
+    NMSupplicantInterfaceState new_state = new_state_i;
+    NMSupplicantInterfaceState old_state = old_state_i;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "P2P Group supplicant interface state: %s -> %s",
+          nm_supplicant_interface_state_to_string(old_state),
+          nm_supplicant_interface_state_to_string(new_state));
+
+    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
+        supplicant_group_interface_release(self);
+
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_DISCONNECTED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+        return;
+    }
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        supplicant_group_iface_is_ready(self);
+        return;
+    }
+}
+
+static void
+supplicant_group_iface_group_finished_cb(NMSupplicantInterface *iface,
+                                         const char *           iface_path,
+                                         void *                 user_data)
+{
+    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data);
+
+    supplicant_group_interface_release(self);
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_DISCONNECTED,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+}
+
+static void
+supplicant_iface_group_joined_updated_cb(NMSupplicantInterface *iface,
+                                         GParamSpec *           pspec,
+                                         void *                 user_data)
+{
+    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(user_data);
+
+    check_group_iface_ready(self);
+}
+
+static void
+supplicant_iface_group_started_cb(NMSupplicantInterface *iface,
+                                  NMSupplicantInterface *group_iface,
+                                  NMDeviceWifiP2P *      self)
+{
+    NMDeviceWifiP2PPrivate *   priv;
+    NMSupplicantInterfaceState state;
+
+    g_return_if_fail(self);
+
+    if (!nm_device_is_activating(NM_DEVICE(self))) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "P2P: WPA supplicant notified a group start but we are not trying to connect! "
+              "Ignoring the event.");
+        return;
+    }
+
+    priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    supplicant_group_interface_release(self);
+
+    priv->group_iface = g_object_ref(group_iface);
+
+    /* We need to wait for the interface to be ready and the group
+     * information to be resolved. */
+    g_signal_connect(priv->group_iface,
+                     "notify::" NM_SUPPLICANT_INTERFACE_P2P_GROUP_JOINED,
+                     G_CALLBACK(supplicant_iface_group_joined_updated_cb),
+                     self);
+
+    g_signal_connect(priv->group_iface,
+                     NM_SUPPLICANT_INTERFACE_STATE,
+                     G_CALLBACK(supplicant_group_iface_state_cb),
+                     self);
+
+    g_signal_connect(priv->group_iface,
+                     NM_SUPPLICANT_INTERFACE_GROUP_FINISHED,
+                     G_CALLBACK(supplicant_group_iface_group_finished_cb),
+                     self);
+
+    state = nm_supplicant_interface_get_state(priv->group_iface);
+    if (state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        _set_is_waiting_for_supplicant(self, TRUE);
+        return;
+    }
+
+    supplicant_group_iface_is_ready(self);
+}
+
+static void
+supplicant_group_interface_release(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (!priv->group_iface)
+        return;
+
+    g_signal_handlers_disconnect_by_data(priv->group_iface, self);
+
+    nm_supplicant_interface_p2p_disconnect(priv->group_iface);
+
+    g_clear_object(&priv->group_iface);
+}
+
+static void
+supplicant_interfaces_release(NMDeviceWifiP2P *self, gboolean set_is_waiting)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->peer_dump_id);
+
+    remove_all_peers(self);
+
+    if (priv->mgmt_iface) {
+        _LOGD(LOGD_DEVICE | LOGD_WIFI, "P2P: Releasing WPA supplicant interface.");
+        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
+        g_signal_handlers_disconnect_by_data(priv->mgmt_iface, self);
+        g_clear_object(&priv->mgmt_iface);
+        nm_clear_g_source(&priv->find_peer_timeout_id);
+        nm_clear_g_source(&priv->sup_timeout_id);
+    }
+
+    supplicant_group_interface_release(self);
+
+    if (set_is_waiting)
+        _set_is_waiting_for_supplicant(self, TRUE);
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(device);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    update_disconnect_on_connection_peer_missing(self);
+
+    if (new_state <= NM_DEVICE_STATE_UNAVAILABLE) {
+        /* Clean up the supplicant interface because in these states the
+         * device cannot be used.
+         * Do not clean up for the UNMANAGED to UNAVAILABLE transition which
+         * will happen during initialization.
+         */
+        if (priv->mgmt_iface && old_state > new_state)
+            supplicant_interfaces_release(self, TRUE);
+
+        /* TODO: More cleanup needed? */
+    }
+
+    switch (new_state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+        break;
+    case NM_DEVICE_STATE_UNAVAILABLE:
+        if (!priv->mgmt_iface
+            || !nm_supplicant_interface_state_is_operational(
+                nm_supplicant_interface_get_state(priv->mgmt_iface)))
+            _set_is_waiting_for_supplicant(self, TRUE);
+        break;
+    case NM_DEVICE_STATE_NEED_AUTH:
+        /* Disconnect? */
+        break;
+    case NM_DEVICE_STATE_IP_CHECK:
+        /* Clear any critical protocol notification in the wifi stack */
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     nm_device_get_ip_ifindex(device),
+                                                     FALSE);
+        break;
+    case NM_DEVICE_STATE_ACTIVATED:
+        //activation_success_handler (device);
+        break;
+    case NM_DEVICE_STATE_FAILED:
+        /* Clear any critical protocol notification in the wifi stack.
+         * At this point the IP device may have been removed already. */
+        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
+        if (nm_device_get_ip_ifindex(device) > 0)
+            nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                         nm_device_get_ip_ifindex(device),
+                                                         FALSE);
+        break;
+    case NM_DEVICE_STATE_DISCONNECTED:
+        nm_supplicant_manager_set_wfd_ies(priv->sup_mgr, NULL);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+impl_device_wifi_p2p_start_find(NMDBusObject *                     obj,
+                                const NMDBusInterfaceInfoExtended *interface_info,
+                                const NMDBusMethodInfoExtended *   method_info,
+                                GDBusConnection *                  connection,
+                                const char *                       sender,
+                                GDBusMethodInvocation *            invocation,
+                                GVariant *                         parameters)
+{
+    NMDeviceWifiP2P *       self       = NM_DEVICE_WIFI_P2P(obj);
+    NMDeviceWifiP2PPrivate *priv       = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    gs_unref_variant GVariant *options = NULL;
+    const char *               opts_key;
+    GVariant *                 opts_val;
+    GVariantIter               iter;
+    gint32                     timeout = 30;
+
+    g_variant_get(parameters, "(@a{sv})", &options);
+
+    g_variant_iter_init(&iter, options);
+    while (g_variant_iter_next(&iter, "{&sv}", &opts_key, &opts_val)) {
+        _nm_unused gs_unref_variant GVariant *opts_val_free = opts_val;
+
+        if (nm_streq(opts_key, "timeout")) {
+            if (!g_variant_is_of_type(opts_val, G_VARIANT_TYPE_INT32)) {
+                g_dbus_method_invocation_return_error_literal(
+                    invocation,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                    "\"timeout\" must be an integer \"i\"");
+                return;
+            }
+
+            timeout = g_variant_get_int32(opts_val);
+            if (timeout <= 0 || timeout > 600) {
+                g_dbus_method_invocation_return_error_literal(
+                    invocation,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_NOT_ALLOWED,
+                    "The timeout for a find operation needs to be in the range of 1-600s.");
+                return;
+            }
+
+            continue;
+        }
+
+        g_dbus_method_invocation_return_error(invocation,
+                                              NM_DEVICE_ERROR,
+                                              NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                                              "Unsupported options key \"%s\"",
+                                              opts_key);
+        return;
+    }
+
+    if (!priv->mgmt_iface) {
+        g_dbus_method_invocation_return_error_literal(
+            invocation,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_NOT_ACTIVE,
+            "WPA Supplicant management interface is currently unavailable.");
+        return;
+    }
+
+    nm_supplicant_interface_p2p_start_find(priv->mgmt_iface, timeout);
+
+    g_dbus_method_invocation_return_value(invocation, NULL);
+}
+
+static void
+impl_device_wifi_p2p_stop_find(NMDBusObject *                     obj,
+                               const NMDBusInterfaceInfoExtended *interface_info,
+                               const NMDBusMethodInfoExtended *   method_info,
+                               GDBusConnection *                  connection,
+                               const char *                       sender,
+                               GDBusMethodInvocation *            invocation,
+                               GVariant *                         parameters)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(obj);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (!priv->mgmt_iface) {
+        g_dbus_method_invocation_return_error_literal(
+            invocation,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_NOT_ACTIVE,
+            "WPA Supplicant management interface is currently unavailable.");
+        return;
+    }
+
+    nm_supplicant_interface_p2p_stop_find(priv->mgmt_iface);
+
+    g_dbus_method_invocation_return_value(invocation, NULL);
+}
+
+/*****************************************************************************/
+
+NMSupplicantInterface *
+nm_device_wifi_p2p_get_mgmt_iface(NMDeviceWifiP2P *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_WIFI_P2P(self), NULL);
+
+    return NM_DEVICE_WIFI_P2P_GET_PRIVATE(self)->mgmt_iface;
+}
+
+void
+nm_device_wifi_p2p_set_mgmt_iface(NMDeviceWifiP2P *self, NMSupplicantInterface *iface)
+{
+    NMDeviceWifiP2PPrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE_WIFI_P2P(self));
+    g_return_if_fail(!iface || NM_IS_SUPPLICANT_INTERFACE(iface));
+
+    priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    if (priv->mgmt_iface == iface)
+        goto done;
+
+    supplicant_interfaces_release(self, FALSE);
+
+    if (!iface)
+        goto done;
+
+    _LOGD(LOGD_DEVICE | LOGD_WIFI,
+          "P2P: WPA supplicant management interface changed to %s.",
+          nm_ref_string_get_str(nm_supplicant_interface_get_object_path(iface)));
+
+    priv->mgmt_iface = g_object_ref(iface);
+
+    g_signal_connect(priv->mgmt_iface,
+                     NM_SUPPLICANT_INTERFACE_STATE,
+                     G_CALLBACK(supplicant_iface_state_cb),
+                     self);
+    g_signal_connect(priv->mgmt_iface,
+                     NM_SUPPLICANT_INTERFACE_PEER_CHANGED,
+                     G_CALLBACK(supplicant_iface_peer_changed_cb),
+                     self);
+    g_signal_connect(priv->mgmt_iface,
+                     NM_SUPPLICANT_INTERFACE_GROUP_STARTED,
+                     G_CALLBACK(supplicant_iface_group_started_cb),
+                     self);
+done:
+    nm_device_queue_recheck_available(NM_DEVICE(self),
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+    _set_is_waiting_for_supplicant(self,
+                                   !priv->mgmt_iface
+                                       || !nm_supplicant_interface_state_is_operational(
+                                           nm_supplicant_interface_get_state(priv->mgmt_iface)));
+}
+
+void
+nm_device_wifi_p2p_remove(NMDeviceWifiP2P *self)
+{
+    g_signal_emit_by_name(self, NM_DEVICE_REMOVED);
+}
+
+/*****************************************************************************/
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    return "wifi-p2p";
+}
+
+/*****************************************************************************/
+
+static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_added = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
+    "PeerAdded",
+    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), );
+
+static const GDBusSignalInfo nm_signal_info_wifi_p2p_peer_removed =
+    NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
+        "PeerRemoved",
+        .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("peer", "o"), ), );
+
+static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_WIFI_P2P,
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "StartFind",
+                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("options", "a{sv}"), ), ),
+                .handle = impl_device_wifi_p2p_start_find, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("StopFind", ),
+                                                .handle = impl_device_wifi_p2p_stop_find, ), ),
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_wifi_p2p_peer_added,
+                                                &nm_signal_info_wifi_p2p_peer_removed, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Peers",
+                                                           "ao",
+                                                           NM_DEVICE_WIFI_P2P_PEERS), ), ),
+    .legacy_property_changed = FALSE,
+};
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(object);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+    const char **           list;
+
+    switch (prop_id) {
+    case PROP_PEERS:
+        list = nm_wifi_p2p_peers_get_paths(&priv->peers_lst_head);
+        g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_wifi_p2p_init(NMDeviceWifiP2P *self)
+{
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
+
+    c_list_init(&priv->peers_lst_head);
+
+    priv->sup_mgr = g_object_ref(nm_supplicant_manager_get());
+}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceWifiP2P *self = NM_DEVICE_WIFI_P2P(object);
+
+    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->constructed(object);
+
+    _set_is_waiting_for_supplicant(self, TRUE);
+}
+
+NMDeviceWifiP2P *
+nm_device_wifi_p2p_new(const char *iface)
+{
+    return g_object_new(NM_TYPE_DEVICE_WIFI_P2P,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "802.11 Wi-Fi P2P",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_WIFI_P2P,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_WIFI,
+                        NM_DEVICE_RFKILL_TYPE,
+                        RFKILL_TYPE_WLAN,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceWifiP2P *       self = NM_DEVICE_WIFI_P2P(object);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(object);
+
+    g_clear_object(&priv->sup_mgr);
+
+    supplicant_interfaces_release(self, FALSE);
+
+    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceWifiP2P *       peer = NM_DEVICE_WIFI_P2P(object);
+    NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(peer);
+
+    nm_assert(c_list_is_empty(&priv->peers_lst_head));
+
+    G_OBJECT_CLASS(nm_device_wifi_p2p_parent_class)->finalize(object);
+}
+
+static void
+nm_device_wifi_p2p_class_init(NMDeviceWifiP2PClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_wifi_p2p);
+
+    device_class->connection_type_supported        = NM_SETTING_WIFI_P2P_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_WIFI_P2P_SETTING_NAME;
+    device_class->link_types           = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI_P2P);
+    device_class->get_type_description = get_type_description;
+
+    /* Do we need compatibility checking or is the default good enough? */
+    device_class->is_available                = is_available;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->complete_connection         = complete_connection;
+
+    device_class->act_stage1_prepare         = act_stage1_prepare;
+    device_class->act_stage2_config          = act_stage2_config;
+    device_class->get_configured_mtu         = get_configured_mtu;
+    device_class->get_auto_ip_config_method  = get_auto_ip_config_method;
+    device_class->act_stage3_ip_config_start = act_stage3_ip_config_start;
+
+    device_class->deactivate        = deactivate;
+    device_class->unmanaged_on_quit = unmanaged_on_quit;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_PEERS] = g_param_spec_boxed(NM_DEVICE_WIFI_P2P_PEERS,
+                                                    "",
+                                                    "",
+                                                    G_TYPE_STRV,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.h b/src/core/devices/wifi/nm-device-wifi-p2p.h
new file mode 100644
index 00000000..d1aadd8e
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_DEVICE_WIFI_P2P_H__
+#define __NM_DEVICE_WIFI_P2P_H__
+
+#include "devices/nm-device.h"
+#include "supplicant/nm-supplicant-interface.h"
+
+#define NM_TYPE_DEVICE_WIFI_P2P (nm_device_wifi_p2p_get_type())
+#define NM_DEVICE_WIFI_P2P(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2P))
+#define NM_DEVICE_WIFI_P2P_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass))
+#define NM_IS_DEVICE_WIFI_P2P(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI_P2P))
+#define NM_IS_DEVICE_WIFI_P2P_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIFI_P2P))
+#define NM_DEVICE_WIFI_P2P_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIFI_P2P, NMDeviceWifiP2PClass))
+
+#define NM_DEVICE_WIFI_P2P_PEERS  "peers"
+#define NM_DEVICE_WIFI_P2P_GROUPS "groups"
+
+typedef struct _NMDeviceWifiP2P      NMDeviceWifiP2P;
+typedef struct _NMDeviceWifiP2PClass NMDeviceWifiP2PClass;
+
+GType nm_device_wifi_p2p_get_type(void);
+
+NMDeviceWifiP2P *nm_device_wifi_p2p_new(const char *iface);
+
+NMSupplicantInterface *nm_device_wifi_p2p_get_mgmt_iface(NMDeviceWifiP2P *self);
+void nm_device_wifi_p2p_set_mgmt_iface(NMDeviceWifiP2P *self, NMSupplicantInterface *iface);
+
+void nm_device_wifi_p2p_remove(NMDeviceWifiP2P *self);
+
+#endif /* __NM_DEVICE_WIFI_P2P_H__ */
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
new file mode 100644
index 00000000..042d4887
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -0,0 +1,3879 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2017 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-wifi.h"
+
+#include <netinet/in.h>
+#include <unistd.h>
+#include <linux/if_ether.h>
+
+#include "nm-glib-aux/nm-ref-string.h"
+#include "nm-glib-aux/nm-c-list.h"
+#include "nm-device-wifi-p2p.h"
+#include "nm-wifi-ap.h"
+#include "nm-libnm-core-intern/nm-common-macros.h"
+#include "devices/nm-device.h"
+#include "devices/nm-device-private.h"
+#include "nm-dbus-manager.h"
+#include "nm-utils.h"
+#include "NetworkManagerUtils.h"
+#include "nm-act-request.h"
+#include "supplicant/nm-supplicant-manager.h"
+#include "supplicant/nm-supplicant-interface.h"
+#include "supplicant/nm-supplicant-config.h"
+#include "nm-setting-connection.h"
+#include "nm-setting-wireless.h"
+#include "nm-setting-wireless-security.h"
+#include "nm-setting-8021x.h"
+#include "nm-setting-ip4-config.h"
+#include "nm-ip4-config.h"
+#include "nm-setting-ip6-config.h"
+#include "platform/nm-platform.h"
+#include "nm-auth-utils.h"
+#include "settings/nm-settings-connection.h"
+#include "settings/nm-settings.h"
+#include "nm-wifi-utils.h"
+#include "nm-wifi-common.h"
+#include "nm-core-internal.h"
+#include "nm-config.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceWifi
+#include "devices/nm-device-logging.h"
+
+#define SCAN_INTERVAL_SEC_MIN  3
+#define SCAN_INTERVAL_SEC_STEP 20
+#define SCAN_INTERVAL_SEC_MAX  120
+
+#define SCAN_EXTRA_DELAY_MSEC 500
+
+#define SCAN_RAND_MAC_ADDRESS_EXPIRE_SEC (5 * 60)
+
+#define SCAN_REQUEST_SSIDS_MAX_NUM      32u
+#define SCAN_REQUEST_SSIDS_MAX_AGE_MSEC (3 * 60 * NM_UTILS_MSEC_PER_SEC)
+
+#define _LOGT_scan(...) _LOGT(LOGD_WIFI_SCAN, "wifi-scan: " __VA_ARGS__)
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceWifi,
+                             PROP_MODE,
+                             PROP_BITRATE,
+                             PROP_ACCESS_POINTS,
+                             PROP_ACTIVE_ACCESS_POINT,
+                             PROP_CAPABILITIES,
+                             PROP_SCANNING,
+                             PROP_LAST_SCAN, );
+
+enum {
+    P2P_DEVICE_CREATED,
+
+    LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    CList       aps_lst_head;
+    GHashTable *aps_idx_by_supplicant_path;
+
+    CList scanning_prohibited_lst_head;
+
+    GCancellable *scan_request_cancellable;
+
+    GSource *scan_request_delay_source;
+
+    NMWifiAP *current_ap;
+
+    GHashTable *scan_request_ssids_hash;
+    CList       scan_request_ssids_lst_head;
+
+    NMActRequestGetSecretsCallId *wifi_secrets_id;
+
+    NMSupplicantManager *        sup_mgr;
+    NMSupplMgrCreateIfaceHandle *sup_create_handle;
+    NMSupplicantInterface *      sup_iface;
+
+    gint64 scan_last_complete_msec;
+    gint64 scan_periodic_next_msec;
+
+    gint64 scan_last_request_started_at_msec;
+
+    guint scan_kickoff_timeout_id;
+
+    guint ap_dump_id;
+
+    guint periodic_update_id;
+
+    guint link_timeout_id;
+    guint reacquire_iface_id;
+    guint wps_timeout_id;
+    guint sup_timeout_id; /* supplicant association timeout */
+
+    NMDeviceWifiCapabilities    capabilities;
+    NMSettingWirelessWakeOnWLan wowlan_restore;
+
+    NMDeviceWifiP2P *p2p_device;
+    NM80211Mode      mode;
+
+    guint32 failed_iface_count;
+    gint32  hw_addr_scan_expire;
+
+    guint32 rate;
+
+    guint8 scan_periodic_interval_sec;
+
+    bool enabled : 1; /* rfkilled or not */
+    bool scan_is_scanning : 1;
+    bool scan_periodic_allowed : 1;
+    bool scan_explicit_allowed : 1;
+    bool scan_explicit_requested : 1;
+    bool ssid_found : 1;
+    bool hidden_probe_scan_warn : 1;
+
+} NMDeviceWifiPrivate;
+
+struct _NMDeviceWifi {
+    NMDevice            parent;
+    NMDeviceWifiPrivate _priv;
+};
+
+struct _NMDeviceWifiClass {
+    NMDeviceClass parent;
+};
+
+/*****************************************************************************/
+
+G_DEFINE_TYPE(NMDeviceWifi, nm_device_wifi, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_WIFI_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceWifi, NM_IS_DEVICE_WIFI, NMDevice)
+
+/*****************************************************************************/
+
+static void supplicant_iface_state_down(NMDeviceWifi *self);
+
+static void cleanup_association_attempt(NMDeviceWifi *self, gboolean disconnect);
+
+static void supplicant_iface_state(NMDeviceWifi *             self,
+                                   NMSupplicantInterfaceState new_state,
+                                   NMSupplicantInterfaceState old_state,
+                                   int                        disconnect_reason,
+                                   gboolean                   is_real_signal);
+
+static void supplicant_iface_state_cb(NMSupplicantInterface *iface,
+                                      int                    new_state_i,
+                                      int                    old_state_i,
+                                      int                    disconnect_reason,
+                                      gpointer               user_data);
+
+static void supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface,
+                                            NMSupplicantBssInfo *  bss_info,
+                                            gboolean               is_present,
+                                            NMDeviceWifi *         self);
+
+static void supplicant_iface_wps_credentials_cb(NMSupplicantInterface *iface,
+                                                GVariant *             credentials,
+                                                NMDeviceWifi *         self);
+
+static void supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
+                                                GParamSpec *           pspec,
+                                                NMDeviceWifi *         self);
+
+static void supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface,
+                                                  GParamSpec *           pspec,
+                                                  NMDeviceWifi *         self);
+
+static void periodic_update(NMDeviceWifi *self);
+
+static void ap_add_remove(NMDeviceWifi *self,
+                          gboolean      is_adding,
+                          NMWifiAP *    ap,
+                          gboolean      recheck_available_connections);
+
+static void _hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset);
+
+static void recheck_p2p_availability(NMDeviceWifi *self);
+
+static void _scan_kickoff(NMDeviceWifi *self);
+
+static gboolean _scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff);
+
+/*****************************************************************************/
+
+typedef struct {
+    GBytes *ssid;
+    CList   lst;
+    gint64  timestamp_msec;
+} ScanRequestSsidData;
+
+static void
+_scan_request_ssids_remove(ScanRequestSsidData *srs_data)
+{
+    c_list_unlink_stale(&srs_data->lst);
+    g_bytes_unref(srs_data->ssid);
+    nm_g_slice_free(srs_data);
+}
+
+static void
+_scan_request_ssids_remove_with_hash(NMDeviceWifiPrivate *priv, ScanRequestSsidData *srs_data)
+{
+    nm_assert(srs_data);
+    nm_assert(nm_g_hash_table_lookup(priv->scan_request_ssids_hash, srs_data) == srs_data);
+    if (!g_hash_table_remove(priv->scan_request_ssids_hash, srs_data))
+        nm_assert_not_reached();
+    _scan_request_ssids_remove(srs_data);
+}
+
+static void
+_scan_request_ssids_remove_all(NMDeviceWifiPrivate *priv,
+                               gint64               cutoff_with_now_msec,
+                               guint                cutoff_at_len)
+{
+    ScanRequestSsidData *srs_data;
+
+    nm_assert((!priv->scan_request_ssids_hash)
+              == c_list_is_empty(&priv->scan_request_ssids_lst_head));
+    if (!priv->scan_request_ssids_hash)
+        return;
+
+    if (cutoff_at_len == 0) {
+        nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy);
+        while (
+            (srs_data =
+                 c_list_first_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst)))
+            _scan_request_ssids_remove(srs_data);
+        return;
+    }
+
+    if (cutoff_with_now_msec != 0) {
+        gint64 cutoff_time_msec;
+
+        /* remove all entries that are older than a max-age. */
+        nm_assert(cutoff_with_now_msec > 0);
+        cutoff_time_msec = cutoff_with_now_msec - SCAN_REQUEST_SSIDS_MAX_AGE_MSEC;
+        while (
+            (srs_data =
+                 c_list_last_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst))) {
+            if (srs_data->timestamp_msec > cutoff_time_msec)
+                break;
+            _scan_request_ssids_remove_with_hash(priv, srs_data);
+        }
+    }
+
+    if (cutoff_at_len != G_MAXUINT) {
+        guint i;
+
+        /* trim the list to cutoff_at_len elements. */
+        i = nm_g_hash_table_size(priv->scan_request_ssids_hash);
+        for (; i > cutoff_at_len; i--) {
+            ScanRequestSsidData *d;
+
+            d = c_list_last_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst);
+            _scan_request_ssids_remove_with_hash(priv, d);
+        }
+    }
+
+    nm_assert(nm_g_hash_table_size(priv->scan_request_ssids_hash) <= SCAN_REQUEST_SSIDS_MAX_NUM);
+    nm_assert(nm_g_hash_table_size(priv->scan_request_ssids_hash)
+              == c_list_length(&priv->scan_request_ssids_lst_head));
+    if (c_list_is_empty(&priv->scan_request_ssids_lst_head))
+        nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy);
+}
+
+static GPtrArray *
+_scan_request_ssids_fetch(NMDeviceWifiPrivate *priv, gint64 now_msec)
+{
+    ScanRequestSsidData *srs_data;
+    GPtrArray *          ssids;
+    guint                len;
+
+    _scan_request_ssids_remove_all(priv, now_msec, G_MAXUINT);
+
+    len = nm_g_hash_table_size(priv->scan_request_ssids_hash);
+    if (len == 0)
+        return NULL;
+
+    ssids = g_ptr_array_new_full(len, (GDestroyNotify) g_bytes_unref);
+    nm_clear_pointer(&priv->scan_request_ssids_hash, g_hash_table_destroy);
+    while ((srs_data =
+                c_list_first_entry(&priv->scan_request_ssids_lst_head, ScanRequestSsidData, lst))) {
+        g_ptr_array_add(ssids, g_steal_pointer(&srs_data->ssid));
+        _scan_request_ssids_remove(srs_data);
+    }
+    return ssids;
+}
+
+static void
+_scan_request_ssids_track(NMDeviceWifiPrivate *priv, const GPtrArray *ssids)
+{
+    CList  old_lst_head;
+    gint64 now_msec;
+    guint  i;
+
+    if (!ssids || ssids->len == 0)
+        return;
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    if (!priv->scan_request_ssids_hash)
+        priv->scan_request_ssids_hash = g_hash_table_new(nm_pgbytes_hash, nm_pgbytes_equal);
+
+    /* Do a little dance. New elements shall keep their order as in @ssids, but all
+     * new elements should be sorted in the list preexisting elements of the list.
+     * First move the old elements away, and splice them back afterwards. */
+    c_list_init(&old_lst_head);
+    c_list_splice(&old_lst_head, &priv->scan_request_ssids_lst_head);
+
+    for (i = 0; i < ssids->len; i++) {
+        GBytes *             ssid = ssids->pdata[i];
+        ScanRequestSsidData *d;
+
+        G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(ScanRequestSsidData, ssid) == 0);
+        d = g_hash_table_lookup(priv->scan_request_ssids_hash, &ssid);
+        if (!d) {
+            d  = g_slice_new(ScanRequestSsidData);
+            *d = (ScanRequestSsidData){
+                .lst            = C_LIST_INIT(d->lst),
+                .timestamp_msec = now_msec,
+                .ssid           = g_bytes_ref(ssid),
+            };
+            g_hash_table_add(priv->scan_request_ssids_hash, d);
+        } else
+            d->timestamp_msec = now_msec;
+        c_list_link_tail(&priv->scan_request_ssids_lst_head, &d->lst);
+    }
+
+    c_list_splice(&priv->scan_request_ssids_lst_head, &old_lst_head);
+
+    /* Trim the excess. After our splice with old_lst_head, the list contains the new
+     * elements (from @ssids) at the front (in there original order), followed by older elements. */
+    _scan_request_ssids_remove_all(priv, now_msec, SCAN_REQUEST_SSIDS_MAX_NUM);
+}
+
+/*****************************************************************************/
+
+void
+nm_device_wifi_scanning_prohibited_track(NMDeviceWifi *self,
+                                         gpointer      tag,
+                                         gboolean      temporarily_prohibited)
+{
+    NMDeviceWifiPrivate *priv;
+    NMCListElem *        elem;
+
+    g_return_if_fail(NM_IS_DEVICE_WIFI(self));
+    nm_assert(tag);
+
+    priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    /* We track these with a simple CList. This would be not efficient, if
+     * there would be many users that need to be tracked at the same time (there
+     * aren't). In fact, most of the time there is no NMDeviceOlpcMesh and
+     * nobody tracks itself here. Optimize for that and simplicity. */
+
+    elem = nm_c_list_elem_find_first(&priv->scanning_prohibited_lst_head, iter, iter == tag);
+
+    if (!temporarily_prohibited) {
+        if (!elem)
+            return;
+        nm_c_list_elem_free(elem);
+    } else {
+        if (elem)
+            return;
+        c_list_link_tail(&priv->scanning_prohibited_lst_head, &nm_c_list_elem_new_stale(tag)->lst);
+    }
+
+    _scan_notify_allowed(self, NM_TERNARY_DEFAULT);
+}
+
+/*****************************************************************************/
+
+static void
+_ap_dump(NMDeviceWifi *  self,
+         NMLogLevel      log_level,
+         const NMWifiAP *ap,
+         const char *    prefix,
+         gint64          now_msec)
+{
+    char buf[1024];
+
+    buf[0] = '\0';
+    _NMLOG(log_level,
+           LOGD_WIFI_SCAN,
+           "wifi-ap: %-7s %s",
+           prefix,
+           nm_wifi_ap_to_string(ap, buf, sizeof(buf), now_msec));
+}
+
+gboolean
+nm_device_wifi_get_scanning(NMDeviceWifi *self)
+{
+    g_return_val_if_fail(NM_IS_DEVICE_WIFI(self), FALSE);
+
+    return NM_DEVICE_WIFI_GET_PRIVATE(self)->scan_is_scanning;
+}
+
+static gboolean
+_scan_is_scanning_eval(NMDeviceWifiPrivate *priv)
+{
+    return priv->scan_request_cancellable || priv->scan_request_delay_source
+           || (priv->sup_iface && nm_supplicant_interface_get_scanning(priv->sup_iface));
+}
+
+static gboolean
+_scan_notify_is_scanning(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv              = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gboolean             last_scan_changed = FALSE;
+    NMDeviceState        state;
+    gboolean             scanning;
+
+    scanning = _scan_is_scanning_eval(priv);
+    if (scanning == priv->scan_is_scanning)
+        return FALSE;
+
+    priv->scan_is_scanning = scanning;
+
+    if (!scanning || priv->scan_last_complete_msec == 0) {
+        last_scan_changed             = TRUE;
+        priv->scan_last_complete_msec = nm_utils_get_monotonic_timestamp_msec();
+    }
+
+    _LOGD(LOGD_WIFI,
+          "wifi-scan: scanning-state: %s%s",
+          scanning ? "scanning" : "idle",
+          last_scan_changed ? " (notify last-scan)" : "");
+
+    state = nm_device_get_state(NM_DEVICE(self));
+
+    if (scanning) {
+        /* while the device is activating/activated, we don't need the pending
+         * action. The pending action exists to delay startup complete, while
+         * activating that is already achieved via other means. */
+        if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED)
+            nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WIFI_SCAN, FALSE);
+    }
+
+    nm_gobject_notify_together(self, PROP_SCANNING, last_scan_changed ? PROP_LAST_SCAN : PROP_0);
+
+    _scan_kickoff(self);
+
+    if (!_scan_is_scanning_eval(priv)) {
+        if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED)
+            nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+        nm_device_remove_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WIFI_SCAN, FALSE);
+    }
+
+    return TRUE;
+}
+
+static gboolean
+_scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gboolean             explicit_allowed;
+    gboolean             periodic_allowed;
+    NMDeviceState        state;
+    gboolean             changed = FALSE;
+
+    state = nm_device_get_state(NM_DEVICE(self));
+
+    explicit_allowed = FALSE;
+    periodic_allowed = FALSE;
+
+    if (!c_list_is_empty(&priv->scanning_prohibited_lst_head)) {
+        /* something prohibits scanning. */
+    } else if (NM_IN_SET(priv->mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_AP)) {
+        /* Don't scan when a an AP or Ad-Hoc connection is active as it will
+         * disrupt connected clients or peers. */
+    } else if (NM_IN_SET(state, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_FAILED)) {
+        /* Can always scan when disconnected */
+        explicit_allowed = TRUE;
+        periodic_allowed = TRUE;
+    } else if (NM_IN_SET(state, NM_DEVICE_STATE_ACTIVATED)) {
+        /* Prohibit periodic scans when connected; we ask the supplicant to
+         * background scan for us, unless the connection is locked to a specific
+         * BSSID (in which case scanning is effectively disabled). */
+        periodic_allowed = FALSE;
+
+        /* Prohibit scans if the supplicant is busy */
+        if (priv->sup_iface) {
+            explicit_allowed = !NM_IN_SET(nm_supplicant_interface_get_state(priv->sup_iface),
+                                          NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING,
+                                          NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED,
+                                          NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE,
+                                          NM_SUPPLICANT_INTERFACE_STATE_GROUP_HANDSHAKE);
+        } else
+            explicit_allowed = FALSE;
+    }
+
+    if (explicit_allowed != priv->scan_explicit_allowed
+        || periodic_allowed != priv->scan_periodic_allowed) {
+        priv->scan_periodic_allowed = periodic_allowed;
+        priv->scan_explicit_allowed = explicit_allowed;
+        _LOGT_scan("scan-periodic-allowed=%d, scan-explicit-allowed=%d",
+                   periodic_allowed,
+                   explicit_allowed);
+        changed = TRUE;
+    }
+
+    if (do_kickoff == NM_TERNARY_TRUE || (do_kickoff == NM_TERNARY_DEFAULT && changed))
+        _scan_kickoff(self);
+
+    return changed;
+}
+
+static void
+supplicant_iface_notify_scanning_cb(NMSupplicantInterface *iface,
+                                    GParamSpec *           pspec,
+                                    NMDeviceWifi *         self)
+{
+    _scan_notify_is_scanning(self);
+}
+
+static gboolean
+unmanaged_on_quit(NMDevice *self)
+{
+    /* Wi-Fi devices cannot be assumed and are always taken down.
+     * However, also when being disconnected, we scan and thus
+     * set the MAC address to a random value.
+     *
+     * We must restore the original MAC address when quitting, thus
+     * signal to unmanage the device. */
+    return TRUE;
+}
+
+static void
+supplicant_interface_acquire_cb(NMSupplicantManager *        supplicant_manager,
+                                NMSupplMgrCreateIfaceHandle *handle,
+                                NMSupplicantInterface *      iface,
+                                GError *                     error,
+                                gpointer                     user_data)
+{
+    NMDeviceWifi *       self = user_data;
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    nm_assert(priv->sup_create_handle == handle);
+
+    priv->sup_create_handle = NULL;
+
+    if (error) {
+        _LOGE(LOGD_WIFI, "Couldn't initialize supplicant interface: %s", error->message);
+        supplicant_iface_state_down(self);
+        nm_device_remove_pending_action(NM_DEVICE(self),
+                                        NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
+                                        TRUE);
+        return;
+    }
+
+    priv->sup_iface = g_object_ref(iface);
+
+    g_signal_connect(priv->sup_iface,
+                     NM_SUPPLICANT_INTERFACE_STATE,
+                     G_CALLBACK(supplicant_iface_state_cb),
+                     self);
+    g_signal_connect(priv->sup_iface,
+                     NM_SUPPLICANT_INTERFACE_BSS_CHANGED,
+                     G_CALLBACK(supplicant_iface_bss_changed_cb),
+                     self);
+    g_signal_connect(priv->sup_iface,
+                     NM_SUPPLICANT_INTERFACE_WPS_CREDENTIALS,
+                     G_CALLBACK(supplicant_iface_wps_credentials_cb),
+                     self);
+    g_signal_connect(priv->sup_iface,
+                     "notify::" NM_SUPPLICANT_INTERFACE_SCANNING,
+                     G_CALLBACK(supplicant_iface_notify_scanning_cb),
+                     self);
+    g_signal_connect(priv->sup_iface,
+                     "notify::" NM_SUPPLICANT_INTERFACE_CURRENT_BSS,
+                     G_CALLBACK(supplicant_iface_notify_current_bss),
+                     self);
+    g_signal_connect(priv->sup_iface,
+                     "notify::" NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE,
+                     G_CALLBACK(supplicant_iface_notify_p2p_available),
+                     self);
+
+    _scan_notify_is_scanning(self);
+
+    if (nm_supplicant_interface_get_state(priv->sup_iface)
+        != NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        /* fake an initial state change. */
+        supplicant_iface_state(user_data,
+                               NM_SUPPLICANT_INTERFACE_STATE_STARTING,
+                               nm_supplicant_interface_get_state(priv->sup_iface),
+                               0,
+                               FALSE);
+    }
+}
+
+static void
+supplicant_interface_acquire(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_assert(!priv->sup_iface);
+    nm_assert(!priv->sup_create_handle);
+
+    priv->sup_create_handle =
+        nm_supplicant_manager_create_interface(priv->sup_mgr,
+                                               nm_device_get_ifindex(NM_DEVICE(self)),
+                                               NM_SUPPLICANT_DRIVER_WIRELESS,
+                                               supplicant_interface_acquire_cb,
+                                               self);
+    nm_device_add_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, TRUE);
+}
+
+static void
+supplicant_interface_release(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (nm_clear_pointer(&priv->sup_create_handle, nm_supplicant_manager_create_interface_cancel))
+        nm_device_remove_pending_action(NM_DEVICE(self),
+                                        NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
+                                        TRUE);
+
+    nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+    nm_clear_g_source_inst(&priv->scan_request_delay_source);
+    nm_clear_g_cancellable(&priv->scan_request_cancellable);
+
+    _scan_request_ssids_remove_all(priv, 0, 0);
+
+    priv->scan_periodic_interval_sec = 0;
+    priv->scan_periodic_next_msec    = 0;
+
+    nm_clear_g_source(&priv->ap_dump_id);
+
+    if (priv->sup_iface) {
+        /* Clear supplicant interface signal handlers */
+        g_signal_handlers_disconnect_by_data(priv->sup_iface, self);
+
+        /* Tell the supplicant to disconnect from the current AP */
+        nm_supplicant_interface_disconnect(priv->sup_iface);
+
+        g_clear_object(&priv->sup_iface);
+    }
+
+    if (priv->p2p_device) {
+        /* Signal to P2P device to also release its reference */
+        nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, NULL);
+    }
+
+    _scan_notify_is_scanning(self);
+}
+
+static void
+update_seen_bssids_cache(NMDeviceWifi *self, NMWifiAP *ap)
+{
+    g_return_if_fail(NM_IS_DEVICE_WIFI(self));
+
+    if (ap == NULL)
+        return;
+
+    /* Don't cache the BSSID for Ad-Hoc APs */
+    if (nm_wifi_ap_get_mode(ap) != NM_802_11_MODE_INFRA)
+        return;
+
+    if (nm_device_get_state(NM_DEVICE(self)) == NM_DEVICE_STATE_ACTIVATED
+        && nm_device_has_unmodified_applied_connection(NM_DEVICE(self),
+                                                       NM_SETTING_COMPARE_FLAG_NONE)) {
+        nm_settings_connection_add_seen_bssid(nm_device_get_settings_connection(NM_DEVICE(self)),
+                                              nm_wifi_ap_get_address(ap));
+    }
+}
+
+static void
+set_current_ap(NMDeviceWifi *self, NMWifiAP *new_ap, gboolean recheck_available_connections)
+{
+    NMDeviceWifiPrivate *priv;
+    NMWifiAP *           old_ap;
+
+    g_return_if_fail(NM_IS_DEVICE_WIFI(self));
+
+    priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    old_ap = priv->current_ap;
+
+    if (old_ap == new_ap)
+        return;
+
+    if (new_ap) {
+        priv->current_ap = g_object_ref(new_ap);
+
+        /* Update seen BSSIDs cache */
+        update_seen_bssids_cache(self, priv->current_ap);
+    } else
+        priv->current_ap = NULL;
+
+    if (old_ap) {
+        NM80211Mode mode = nm_wifi_ap_get_mode(old_ap);
+
+        /* Remove any AP from the internal list if it was created by NM or isn't known to the supplicant */
+        if (NM_IN_SET(mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_AP) || nm_wifi_ap_get_fake(old_ap))
+            ap_add_remove(self, FALSE, old_ap, recheck_available_connections);
+        g_object_unref(old_ap);
+    }
+
+    _notify(self, PROP_ACTIVE_ACCESS_POINT);
+}
+
+static void
+periodic_update(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv;
+    int                  ifindex;
+    guint32              new_rate;
+    int                  percent;
+
+    if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_ACTIVATED) {
+        /* BSSID and signal strength have meaningful values only if the device
+         * is activated and not scanning.
+         */
+        return;
+    }
+
+    priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (!nm_supplicant_interface_state_is_associated(
+            nm_supplicant_interface_get_state(priv->sup_iface))
+        || nm_supplicant_interface_get_scanning(priv->sup_iface)) {
+        /* Only update current AP if we're actually talking to something, otherwise
+         * assume the old one (if any) is still valid until we're told otherwise or
+         * the connection fails.
+         */
+        return;
+    }
+
+    if (priv->mode == NM_802_11_MODE_AP) {
+        /* In AP mode we currently have nothing to do. */
+        return;
+    }
+
+    ifindex = nm_device_get_ifindex(NM_DEVICE(self));
+    if (ifindex <= 0)
+        g_return_if_reached();
+
+    if (priv->current_ap
+        && nm_platform_wifi_get_station(nm_device_get_platform(NM_DEVICE(self)),
+                                        ifindex,
+                                        NULL,
+                                        &percent,
+                                        &new_rate)) {
+        if (nm_wifi_ap_set_strength(priv->current_ap, (gint8) percent)) {
+#if NM_MORE_LOGGING
+            _ap_dump(self, LOGL_TRACE, priv->current_ap, "updated", 0);
+#endif
+        }
+
+        if (new_rate != priv->rate) {
+            priv->rate = new_rate;
+            _notify(self, PROP_BITRATE);
+        }
+    }
+}
+
+static gboolean
+periodic_update_cb(gpointer user_data)
+{
+    periodic_update(user_data);
+    return TRUE;
+}
+
+static void
+ap_add_remove(NMDeviceWifi *self,
+              gboolean      is_adding, /* or else removing */
+              NMWifiAP *    ap,
+              gboolean      recheck_available_connections)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (is_adding) {
+        g_object_ref(ap);
+        ap->wifi_device = NM_DEVICE(self);
+        c_list_link_tail(&priv->aps_lst_head, &ap->aps_lst);
+        if (!g_hash_table_insert(priv->aps_idx_by_supplicant_path,
+                                 nm_wifi_ap_get_supplicant_path(ap),
+                                 ap))
+            nm_assert_not_reached();
+        nm_dbus_object_export(NM_DBUS_OBJECT(ap));
+        _ap_dump(self, LOGL_DEBUG, ap, "added", 0);
+        nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, TRUE);
+    } else {
+        ap->wifi_device = NULL;
+        c_list_unlink(&ap->aps_lst);
+        if (!g_hash_table_remove(priv->aps_idx_by_supplicant_path,
+                                 nm_wifi_ap_get_supplicant_path(ap)))
+            nm_assert_not_reached();
+        _ap_dump(self, LOGL_DEBUG, ap, "removed", 0);
+    }
+
+    _notify(self, PROP_ACCESS_POINTS);
+
+    if (!is_adding) {
+        nm_device_wifi_emit_signal_access_point(NM_DEVICE(self), ap, FALSE);
+        nm_dbus_object_clear_and_unexport(&ap);
+    }
+
+    nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+    if (recheck_available_connections)
+        nm_device_recheck_available_connections(NM_DEVICE(self));
+}
+
+static void
+remove_all_aps(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMWifiAP *           ap;
+
+    if (c_list_is_empty(&priv->aps_lst_head))
+        return;
+
+    set_current_ap(self, NULL, FALSE);
+
+    while ((ap = c_list_first_entry(&priv->aps_lst_head, NMWifiAP, aps_lst)))
+        ap_add_remove(self, FALSE, ap, FALSE);
+
+    nm_device_recheck_available_connections(NM_DEVICE(self));
+}
+
+static gboolean
+wake_on_wlan_restore(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *       priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSettingWirelessWakeOnWLan w;
+
+    w = priv->wowlan_restore;
+    if (w == NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE)
+        return TRUE;
+
+    priv->wowlan_restore = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE;
+    return nm_platform_wifi_set_wake_on_wlan(NM_PLATFORM_GET,
+                                             nm_device_get_ifindex(NM_DEVICE(self)),
+                                             w);
+}
+
+static void
+disconnect_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data)
+{
+    gs_unref_object NMDeviceWifi *self = NULL;
+    NMDeviceDeactivateCallback    callback;
+    gpointer                      callback_user_data;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+
+    /* error will be freed by sup_iface */
+    callback(NM_DEVICE(self), error, callback_user_data);
+}
+
+static void
+disconnect_cb_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    gs_unref_object NMDeviceWifi *self = NULL;
+    NMDeviceDeactivateCallback    callback;
+    gpointer                      callback_user_data;
+    gs_free_error GError *cancelled_error = NULL;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+
+    g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error);
+    callback(NM_DEVICE(self), cancelled_error, callback_user_data);
+}
+
+static void
+deactivate_async(NMDevice *                 device,
+                 GCancellable *             cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   callback_user_data)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gpointer             user_data;
+
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+
+    user_data = nm_utils_user_data_pack(g_object_ref(self), callback, callback_user_data);
+    if (!priv->sup_iface) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_cb_on_idle, user_data);
+        return;
+    }
+
+    cleanup_association_attempt(self, FALSE);
+
+    nm_supplicant_interface_disconnect_async(priv->sup_iface,
+                                             cancellable,
+                                             disconnect_cb,
+                                             user_data);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceWifi *       self    = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv    = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    int                  ifindex = nm_device_get_ifindex(device);
+
+    nm_clear_g_source(&priv->periodic_update_id);
+
+    cleanup_association_attempt(self, TRUE);
+
+    priv->rate = 0;
+
+    set_current_ap(self, NULL, TRUE);
+
+    if (!wake_on_wlan_restore(self))
+        _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot unconfigure WoWLAN.");
+
+    /* Clear any critical protocol notification in the Wi-Fi stack */
+    nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), ifindex, FALSE);
+
+    /* Ensure we're in infrastructure mode after deactivation; some devices
+     * (usually older ones) don't scan well in adhoc mode.
+     */
+    if (nm_platform_wifi_get_mode(nm_device_get_platform(device), ifindex)
+        != NM_802_11_MODE_INFRA) {
+        nm_device_take_down(NM_DEVICE(self), TRUE);
+        nm_platform_wifi_set_mode(nm_device_get_platform(device), ifindex, NM_802_11_MODE_INFRA);
+        nm_device_bring_up(NM_DEVICE(self), TRUE, NULL);
+    }
+
+    if (priv->mode != NM_802_11_MODE_INFRA) {
+        priv->mode = NM_802_11_MODE_INFRA;
+        _notify(self, PROP_MODE);
+    }
+
+    _scan_notify_allowed(self, NM_TERNARY_TRUE);
+}
+
+static void
+deactivate_reset_hw_addr(NMDevice *device)
+{
+    _hw_addr_set_scanning((NMDeviceWifi *) device, TRUE);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSettingWireless *  s_wireless;
+    const char *         mac;
+    const char *const *  mac_blacklist;
+    int                  i;
+    const char *         mode;
+    const char *         perm_hw_addr;
+
+    if (!NM_DEVICE_CLASS(nm_device_wifi_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+
+    perm_hw_addr = nm_device_get_permanent_hw_address(device);
+    mac          = nm_setting_wireless_get_mac_address(s_wireless);
+    if (perm_hw_addr) {
+        if (mac && !nm_utils_hwaddr_matches(mac, -1, perm_hw_addr, -1)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "device MAC address does not match the profile");
+            return FALSE;
+        }
+
+        /* Check for MAC address blacklist */
+        mac_blacklist = nm_setting_wireless_get_mac_address_blacklist(s_wireless);
+        for (i = 0; mac_blacklist[i]; i++) {
+            if (!nm_utils_hwaddr_valid(mac_blacklist[i], ETH_ALEN)) {
+                g_warn_if_reached();
+                return FALSE;
+            }
+
+            if (nm_utils_hwaddr_matches(mac_blacklist[i], -1, perm_hw_addr, -1)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "MAC address blacklisted");
+                return FALSE;
+            }
+        }
+    } else if (mac) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device has no valid MAC address as required by profile");
+        return FALSE;
+    }
+
+    /* Early exit if supplicant or device doesn't support requested mode */
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0) {
+        if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_ADHOC)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "the device does not support Ad-Hoc networks");
+            return FALSE;
+        }
+    } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0) {
+        if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_AP)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "the device does not support Access Point mode");
+            return FALSE;
+        }
+
+        if (priv->sup_iface) {
+            if (nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_AP)
+                == NM_TERNARY_FALSE) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "wpa_supplicant does not support Access Point mode");
+                return FALSE;
+            }
+        }
+    } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0) {
+        if (!(priv->capabilities & NM_WIFI_DEVICE_CAP_MESH)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "the device does not support Mesh mode");
+            return FALSE;
+        }
+
+        if (priv->sup_iface) {
+            if (nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_MESH)
+                == NM_TERNARY_FALSE) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "wpa_supplicant does not support Mesh mode");
+                return FALSE;
+            }
+        }
+    }
+
+    // FIXME: check channel/freq/band against bands the hardware supports
+    // FIXME: check encryption against device capabilities
+    // FIXME: check bitrate against device capabilities
+
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSettingWireless *  s_wifi;
+    const char *         mode;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wifi, FALSE);
+
+    /* a connection that is available for a certain @specific_object, MUST
+     * also be available in general (without @specific_object). */
+
+    if (specific_object) {
+        NMWifiAP *ap;
+
+        ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object);
+        if (!ap) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "requested access point not found");
+            return FALSE;
+        }
+        if (!nm_wifi_ap_check_compatible(ap, connection)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "requested access point is not compatible with profile");
+            return FALSE;
+        }
+        return TRUE;
+    }
+
+    /* Ad-Hoc, AP and Mesh connections are always available because they may be
+     * started at any time.
+     */
+    mode = nm_setting_wireless_get_mode(s_wifi);
+    if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0
+        || g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0
+        || g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0)
+        return TRUE;
+
+    /* Hidden SSIDs obviously don't always appear in the scan list either.
+     *
+     * For an explicit user-activation-request, a connection is considered
+     * available because for hidden Wi-Fi, clients didn't consistently
+     * set the 'hidden' property to indicate hidden SSID networks.  If
+     * activating but the network isn't available let the device recheck
+     * availability.
+     */
+    if (nm_setting_wireless_get_hidden(s_wifi)
+        || NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_IGNORE_AP))
+        return TRUE;
+
+    if (!nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "no compatible access point found");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSettingWireless *  s_wifi;
+    gs_free char *       ssid_utf8 = NULL;
+    NMWifiAP *           ap;
+    GBytes *             ssid         = NULL;
+    GBytes *             setting_ssid = NULL;
+    gboolean             hidden       = FALSE;
+    const char *         mode;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+
+    mode = s_wifi ? nm_setting_wireless_get_mode(s_wifi) : NULL;
+
+    if (!specific_object) {
+        /* If not given a specific object, we need at minimum an SSID */
+        if (!s_wifi) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                "A 'wireless' setting is required if no AP path was given.");
+            return FALSE;
+        }
+
+        setting_ssid = nm_setting_wireless_get_ssid(s_wifi);
+        if (!setting_ssid || g_bytes_get_size(setting_ssid) == 0) {
+            g_set_error_literal(
+                error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                "A 'wireless' setting with a valid SSID is required if no AP path was given.");
+            return FALSE;
+        }
+
+        if (!nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) {
+            /* Find a compatible AP in the scan list */
+            ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+
+            /* If we still don't have an AP, then the WiFI settings needs to be
+             * fully specified by the client.  Might not be able to find an AP
+             * if the network isn't broadcasting the SSID for example.
+             */
+            if (!ap) {
+                if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error))
+                    return FALSE;
+
+                hidden = TRUE;
+            }
+        } else {
+            if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error))
+                return FALSE;
+            ap = NULL;
+        }
+    } else if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP)) {
+        if (!nm_setting_verify(NM_SETTING(s_wifi), connection, error))
+            return FALSE;
+        ap = NULL;
+    } else {
+        ap = nm_wifi_ap_lookup_for_device(NM_DEVICE(self), specific_object);
+        if (!ap) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_SPECIFIC_OBJECT_NOT_FOUND,
+                        "The access point %s was not in the scan list.",
+                        specific_object);
+            return FALSE;
+        }
+    }
+
+    /* Add a wifi setting if one doesn't exist yet */
+    if (!s_wifi) {
+        s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wifi));
+    }
+
+    if (ap)
+        ssid = nm_wifi_ap_get_ssid(ap);
+
+    if (ssid == NULL) {
+        /* The AP must be hidden.  Connecting to a Wi-Fi AP requires the SSID
+         * as part of the initial handshake, so check the connection details
+         * for the SSID.  The AP object will still be used for encryption
+         * settings and such.
+         */
+        ssid = nm_setting_wireless_get_ssid(s_wifi);
+    }
+
+    if (ssid == NULL) {
+        /* If there's no SSID on the AP itself, and no SSID in the
+         * connection data, then we cannot connect at all.  Return an error.
+         */
+        g_set_error_literal(
+            error,
+            NM_DEVICE_ERROR,
+            NM_DEVICE_ERROR_INVALID_CONNECTION,
+            ap ? "A 'wireless' setting with a valid SSID is required for hidden access points."
+               : "Cannot create 'wireless' setting due to missing SSID.");
+        return FALSE;
+    }
+
+    if (ap) {
+        /* If the SSID is a well-known SSID, lock the connection to the AP's
+         * specific BSSID so NM doesn't autoconnect to some random wifi net.
+         */
+        if (!nm_wifi_ap_complete_connection(ap,
+                                            connection,
+                                            nm_wifi_utils_is_manf_default_ssid(ssid),
+                                            error))
+            return FALSE;
+    }
+
+    ssid_utf8 = _nm_utils_ssid_to_utf8(ssid);
+    nm_utils_complete_generic(
+        nm_device_get_platform(device),
+        connection,
+        NM_SETTING_WIRELESS_SETTING_NAME,
+        existing_connections,
+        ssid_utf8,
+        ssid_utf8,
+        NULL,
+        nm_setting_wireless_get_mac_address(s_wifi) ? NULL : nm_device_get_iface(device),
+        TRUE);
+
+    if (hidden)
+        g_object_set(s_wifi, NM_SETTING_WIRELESS_HIDDEN, TRUE, NULL);
+
+    return TRUE;
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceWifi *             self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *      priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSupplicantInterfaceState supplicant_state;
+
+    if (!priv->enabled)
+        return FALSE;
+
+    if (!priv->sup_iface)
+        return FALSE;
+
+    supplicant_state = nm_supplicant_interface_get_state(priv->sup_iface);
+    if (supplicant_state <= NM_SUPPLICANT_INTERFACE_STATE_STARTING
+        || supplicant_state > NM_SUPPLICANT_INTERFACE_STATE_COMPLETED)
+        return FALSE;
+
+    return TRUE;
+}
+
+static gboolean
+get_autoconnect_allowed(NMDevice *device)
+{
+    return !NM_DEVICE_WIFI_GET_PRIVATE(NM_DEVICE_WIFI(device))->scan_is_scanning;
+}
+
+static gboolean
+can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **specific_object)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMConnection *       connection;
+    NMSettingWireless *  s_wifi;
+    NMWifiAP *           ap;
+    const char *         method6, *mode;
+    gboolean             auto4, auto6;
+
+    nm_assert(!specific_object || !*specific_object);
+
+    if (!NM_DEVICE_CLASS(nm_device_wifi_parent_class)->can_auto_connect(device, sett_conn, NULL))
+        return FALSE;
+
+    connection = nm_settings_connection_get_connection(sett_conn);
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wifi, FALSE);
+
+    /* Always allow autoconnect for AP and non-autoconf Ad-Hoc or Mesh */
+    auto4   = nm_streq0(nm_utils_get_ip_config_method(connection, AF_INET),
+                      NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+    method6 = nm_utils_get_ip_config_method(connection, AF_INET6);
+    auto6   = nm_streq0(method6, NM_SETTING_IP6_CONFIG_METHOD_AUTO)
+            || nm_streq0(method6, NM_SETTING_IP6_CONFIG_METHOD_DHCP);
+
+    mode = nm_setting_wireless_get_mode(s_wifi);
+
+    if (nm_streq0(mode, NM_SETTING_WIRELESS_MODE_AP))
+        return TRUE;
+    else if (!auto4 && nm_streq0(mode, NM_SETTING_WIRELESS_MODE_ADHOC))
+        return TRUE;
+    else if (!auto4 && !auto6 && nm_streq0(mode, NM_SETTING_WIRELESS_MODE_MESH))
+        return TRUE;
+
+    ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+    if (ap) {
+        /* All good; connection is usable */
+        NM_SET_OUT(specific_object, g_strdup(nm_dbus_object_get_path(NM_DBUS_OBJECT(ap))));
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+const CList *
+_nm_device_wifi_get_aps(NMDeviceWifi *self)
+{
+    return &NM_DEVICE_WIFI_GET_PRIVATE(self)->aps_lst_head;
+}
+
+static void
+_hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset)
+{
+    NMDevice *           device = (NMDevice *) self;
+    NMDeviceWifiPrivate *priv;
+    guint32              now;
+    gboolean             randomize;
+
+    g_return_if_fail(NM_IS_DEVICE_WIFI(self));
+
+    if (nm_device_is_activating(device) || nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED)
+        return;
+
+    priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    randomize = nm_config_data_get_device_config_boolean(
+        NM_CONFIG_GET_DATA,
+        NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_RAND_MAC_ADDRESS,
+        device,
+        TRUE,
+        TRUE);
+
+    if (!randomize) {
+        /* expire the temporary MAC address used during scanning */
+        priv->hw_addr_scan_expire = 0;
+
+        if (do_reset) {
+            priv->scan_last_request_started_at_msec = G_MININT64;
+            priv->scan_periodic_next_msec           = 0;
+            priv->scan_periodic_interval_sec        = 0;
+            nm_device_hw_addr_reset(device, "scanning");
+        }
+        return;
+    }
+
+    now = nm_utils_get_monotonic_timestamp_sec();
+
+    if (now >= priv->hw_addr_scan_expire) {
+        gs_free char *generate_mac_address_mask = NULL;
+        gs_free char *hw_addr_scan              = NULL;
+
+        /* the random MAC address for scanning expires after a while.
+         *
+         * We don't bother with to update the MAC address exactly when
+         * it expires, instead on the next scan request, we will generate
+         * a new one.*/
+        priv->hw_addr_scan_expire = now + SCAN_RAND_MAC_ADDRESS_EXPIRE_SEC;
+
+        generate_mac_address_mask = nm_config_data_get_device_config(
+            NM_CONFIG_GET_DATA,
+            NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_GENERATE_MAC_ADDRESS_MASK,
+            device,
+            NULL);
+
+        priv->scan_last_request_started_at_msec = G_MININT64;
+        priv->scan_periodic_next_msec           = 0;
+        priv->scan_periodic_interval_sec        = 0;
+        hw_addr_scan = nm_utils_hw_addr_gen_random_eth(nm_device_get_initial_hw_address(device),
+                                                       generate_mac_address_mask);
+        nm_device_hw_addr_set(device, hw_addr_scan, "scanning", TRUE);
+    }
+}
+
+static GPtrArray *
+ssids_options_to_ptrarray(GVariant *value, GError **error)
+{
+    gs_unref_ptrarray GPtrArray *ssids = NULL;
+    gsize                        num_ssids;
+    gsize                        i;
+
+    nm_assert(g_variant_is_of_type(value, G_VARIANT_TYPE("aay")));
+
+    num_ssids = g_variant_n_children(value);
+    if (num_ssids > 32) {
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                            "too many SSIDs requested to scan");
+        return NULL;
+    }
+
+    if (num_ssids) {
+        ssids = g_ptr_array_new_full(num_ssids, (GDestroyNotify) g_bytes_unref);
+        for (i = 0; i < num_ssids; i++) {
+            gs_unref_variant GVariant *v = NULL;
+            gsize                      len;
+            const guint8 *             bytes;
+
+            v     = g_variant_get_child_value(value, i);
+            bytes = g_variant_get_fixed_array(v, &len, sizeof(guint8));
+            if (len > 32) {
+                g_set_error(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                            "SSID at index %d more than 32 bytes",
+                            (int) i);
+                return NULL;
+            }
+
+            g_ptr_array_add(ssids, g_bytes_new(bytes, len));
+        }
+    }
+
+    return g_steal_pointer(&ssids);
+}
+
+GPtrArray *
+nmtst_ssids_options_to_ptrarray(GVariant *value, GError **error)
+{
+    return ssids_options_to_ptrarray(value, error);
+}
+
+static void
+dbus_request_scan_cb(NMDevice *             device,
+                     GDBusMethodInvocation *context,
+                     NMAuthSubject *        subject,
+                     GError *               error,
+                     gpointer               user_data)
+{
+    NMDeviceWifi *       self          = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv          = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gs_unref_ptrarray GPtrArray *ssids = user_data;
+
+    if (error) {
+        g_dbus_method_invocation_return_gerror(context, error);
+        return;
+    }
+
+    _scan_request_ssids_track(priv, ssids);
+    priv->scan_explicit_requested = TRUE;
+    _scan_kickoff(self);
+    g_dbus_method_invocation_return_value(context, NULL);
+}
+
+void
+_nm_device_wifi_request_scan(NMDeviceWifi *         self,
+                             GVariant *             options,
+                             GDBusMethodInvocation *invocation)
+{
+    NMDeviceWifiPrivate *priv          = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMDevice *           device        = NM_DEVICE(self);
+    gs_unref_ptrarray GPtrArray *ssids = NULL;
+
+    if (options) {
+        gs_unref_variant GVariant *val = g_variant_lookup_value(options, "ssids", NULL);
+
+        if (val) {
+            gs_free_error GError *ssid_error = NULL;
+
+            if (!g_variant_is_of_type(val, G_VARIANT_TYPE("aay"))) {
+                g_dbus_method_invocation_return_error_literal(invocation,
+                                                              NM_DEVICE_ERROR,
+                                                              NM_DEVICE_ERROR_INVALID_ARGUMENT,
+                                                              "Invalid 'ssid' scan option");
+                return;
+            }
+
+            ssids = ssids_options_to_ptrarray(val, &ssid_error);
+            if (ssid_error) {
+                g_dbus_method_invocation_return_gerror(invocation, ssid_error);
+                return;
+            }
+        }
+    }
+
+    if (!priv->enabled || !priv->sup_iface
+        || nm_device_get_state(device) < NM_DEVICE_STATE_DISCONNECTED) {
+        g_dbus_method_invocation_return_error_literal(invocation,
+                                                      NM_DEVICE_ERROR,
+                                                      NM_DEVICE_ERROR_NOT_ALLOWED,
+                                                      "Scanning not allowed while unavailable");
+        return;
+    }
+
+    nm_device_auth_request(device,
+                           invocation,
+                           NULL,
+                           NM_AUTH_PERMISSION_WIFI_SCAN,
+                           TRUE,
+                           NULL,
+                           dbus_request_scan_cb,
+                           g_steal_pointer(&ssids));
+}
+
+static gboolean
+hidden_filter_func(NMSettings *settings, NMSettingsConnection *set_con, gpointer user_data)
+{
+    NMConnection *     connection = nm_settings_connection_get_connection(set_con);
+    NMSettingWireless *s_wifi;
+
+    if (!nm_connection_is_type(connection, NM_SETTING_WIRELESS_SETTING_NAME))
+        return FALSE;
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    if (!s_wifi)
+        return FALSE;
+    if (nm_streq0(nm_setting_wireless_get_mode(s_wifi), NM_SETTING_WIRELESS_MODE_AP))
+        return FALSE;
+    return nm_setting_wireless_get_hidden(s_wifi);
+}
+
+static GPtrArray *
+_scan_request_ssids_build_hidden(NMDeviceWifi *self,
+                                 gint64        now_msec,
+                                 gboolean *    out_has_hidden_profiles)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    guint   max_scan_ssids    = nm_supplicant_interface_get_max_scan_ssids(priv->sup_iface);
+    gs_free NMSettingsConnection **connections  = NULL;
+    gs_unref_ptrarray GPtrArray *ssids          = NULL;
+    gs_unref_hashtable GHashTable *unique_ssids = NULL;
+    guint                          connections_len;
+    guint                          n_hidden;
+    guint                          i;
+
+    NM_SET_OUT(out_has_hidden_profiles, FALSE);
+
+    /* collect all pending explicit SSIDs. */
+    ssids = _scan_request_ssids_fetch(priv, now_msec);
+
+    if (max_scan_ssids == 0) {
+        /* no space. @ssids will be ignored. */
+        return NULL;
+    }
+
+    if (ssids) {
+        if (ssids->len < max_scan_ssids) {
+            /* Add wildcard SSID using a static wildcard SSID used for every scan */
+            g_ptr_array_insert(ssids, 0, g_bytes_ref(nm_gbytes_get_empty()));
+        }
+        if (ssids->len >= max_scan_ssids) {
+            /* there is no more space. Use what we have. */
+            g_ptr_array_set_size(ssids, max_scan_ssids);
+            return g_steal_pointer(&ssids);
+        }
+    }
+
+    connections = nm_settings_get_connections_clone(nm_device_get_settings((NMDevice *) self),
+                                                    &connections_len,
+                                                    hidden_filter_func,
+                                                    NULL,
+                                                    NULL,
+                                                    NULL);
+    if (!connections[0])
+        return g_steal_pointer(&ssids);
+
+    if (!ssids) {
+        ssids = g_ptr_array_new_full(max_scan_ssids, (GDestroyNotify) g_bytes_unref);
+        /* Add wildcard SSID using a static wildcard SSID used for every scan */
+        g_ptr_array_insert(ssids, 0, g_bytes_ref(nm_gbytes_get_empty()));
+    }
+
+    unique_ssids = g_hash_table_new(nm_gbytes_hash, nm_gbytes_equal);
+    for (i = 1; i < ssids->len; i++) {
+        if (!g_hash_table_add(unique_ssids, ssids->pdata[i]))
+            nm_assert_not_reached();
+    }
+
+    g_qsort_with_data(connections,
+                      connections_len,
+                      sizeof(NMSettingsConnection *),
+                      nm_settings_connection_cmp_timestamp_p_with_data,
+                      NULL);
+
+    n_hidden = 0;
+    for (i = 0; i < connections_len; i++) {
+        NMSettingWireless *s_wifi;
+        GBytes *           ssid;
+
+        if (ssids->len >= max_scan_ssids)
+            break;
+
+        if (n_hidden > 4) {
+            /* we allow at most 4 hidden profiles to be actively scanned. The
+             * reason is speed and to not disclose too many SSIDs. */
+            break;
+        }
+
+        s_wifi = nm_connection_get_setting_wireless(
+            nm_settings_connection_get_connection(connections[i]));
+        ssid = nm_setting_wireless_get_ssid(s_wifi);
+
+        if (!g_hash_table_add(unique_ssids, ssid))
+            continue;
+
+        g_ptr_array_add(ssids, g_bytes_ref(ssid));
+        n_hidden++;
+    }
+
+    NM_SET_OUT(out_has_hidden_profiles, n_hidden > 0);
+    return g_steal_pointer(&ssids);
+}
+
+static gboolean
+_scan_request_delay_cb(gpointer user_data)
+{
+    NMDeviceWifi *       self = user_data;
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->scan_request_delay_source);
+
+    _LOGT_scan("scan request completed (after extra delay)");
+
+    _scan_notify_is_scanning(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_scan_supplicant_request_scan_cb(NMSupplicantInterface *supp_iface,
+                                 GCancellable *         cancellable,
+                                 gpointer               user_data)
+{
+    NMDeviceWifi *       self;
+    NMDeviceWifiPrivate *priv;
+
+    if (g_cancellable_is_cancelled(cancellable))
+        return;
+
+    self = user_data;
+    priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    _LOGT_scan("scan request completed (D-Bus request)");
+
+    /* we just completed a scan request, but possibly the supplicant's state is not yet toggled
+     * to "scanning". That means, our internal scanning state "priv->scan_is_scanning" would already
+     * flip to idle, while in a moment the supplicant would toggle the state again.
+     *
+     * Artificially keep the scanning state on, for another SCAN_EXTRA_DELAY_MSEC msec. */
+    nm_clear_g_source_inst(&priv->scan_request_delay_source);
+    priv->scan_request_delay_source =
+        nm_g_source_attach(nm_g_timeout_source_new(SCAN_EXTRA_DELAY_MSEC,
+                                                   G_PRIORITY_DEFAULT,
+                                                   _scan_request_delay_cb,
+                                                   self,
+                                                   NULL),
+                           NULL);
+
+    g_clear_object(&priv->scan_request_cancellable);
+    _scan_notify_is_scanning(self);
+}
+
+static gboolean
+_scan_kickoff_timeout_cb(gpointer user_data)
+{
+    NMDeviceWifi *       self = user_data;
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    priv->scan_kickoff_timeout_id = 0;
+    _scan_kickoff(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_scan_kickoff(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv               = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gs_unref_ptrarray GPtrArray *ssids      = NULL;
+    gboolean                     is_explict = FALSE;
+    NMDeviceState                device_state;
+    gboolean                     has_hidden_profiles;
+    gint64                       now_msec;
+    gint64                       ratelimit_duration_msec;
+
+    if (!priv->sup_iface) {
+        _LOGT_scan("kickoff: don't scan (has no supplicant interface)");
+        return;
+    }
+
+    if (priv->scan_request_cancellable) {
+        _LOGT_scan("kickoff: don't scan (has scan_request_cancellable)");
+        /* We are currently waiting for a scan request to complete. Wait longer. */
+        return;
+    }
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    _scan_request_ssids_remove_all(priv, now_msec, G_MAXUINT);
+
+    device_state = nm_device_get_state(NM_DEVICE(self));
+    if (device_state > NM_DEVICE_STATE_DISCONNECTED && device_state <= NM_DEVICE_STATE_ACTIVATED) {
+        /* while we are activated, we rate limit more. */
+        ratelimit_duration_msec = 8000;
+    } else
+        ratelimit_duration_msec = 1500;
+
+    if (priv->scan_last_request_started_at_msec + ratelimit_duration_msec > now_msec) {
+        _LOGT_scan(
+            "kickoff: don't scan (rate limited for another %d.%03d sec%s)",
+            (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec)
+                   / 1000),
+            (int) ((priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec)
+                   % 1000),
+            !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
+        if (!priv->scan_kickoff_timeout_id
+            && (priv->scan_explicit_allowed || priv->scan_periodic_allowed)) {
+            priv->scan_kickoff_timeout_id = g_timeout_add(priv->scan_last_request_started_at_msec
+                                                              + ratelimit_duration_msec - now_msec,
+                                                          _scan_kickoff_timeout_cb,
+                                                          self);
+        }
+        return;
+    }
+
+    if (priv->scan_last_complete_msec + 200 > now_msec) {
+        gint32 timeout_msec = priv->scan_last_complete_msec + 200 - now_msec;
+
+        /* after a scan just completed, it is ratelimited for another 200 msec. This is in
+         * addition to our rate limiting above (where scanning can take longer than our rate limit
+         * duration).
+         *
+         * This gives the device a chance to autoconnect. Also, if a scanning just completed,
+         * we want to back off a bit before starting again. */
+        _LOGT_scan("kickoff: don't scan (rate limited for another %d.%03d sec after previous scan)",
+                   timeout_msec / 1000,
+                   timeout_msec % 1000);
+        nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+        priv->scan_kickoff_timeout_id = g_timeout_add(timeout_msec, _scan_kickoff_timeout_cb, self);
+        return;
+    }
+
+    if (priv->scan_explicit_requested) {
+        if (!priv->scan_explicit_allowed) {
+            _LOGT_scan("kickoff: don't scan (explicit scan requested but not allowed)");
+            return;
+        }
+        priv->scan_explicit_requested = FALSE;
+        is_explict                    = TRUE;
+    } else {
+        if (!priv->scan_periodic_allowed) {
+            _LOGT_scan("kickoff: don't scan (periodic scan currently not allowed)");
+            priv->scan_periodic_next_msec    = 0;
+            priv->scan_periodic_interval_sec = 0;
+            nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+            return;
+        }
+
+        nm_assert(priv->scan_explicit_allowed);
+
+        if (now_msec < priv->scan_periodic_next_msec) {
+            _LOGT_scan("kickoff: don't scan (periodic scan waiting for another %d.%03d sec%s)",
+                       (int) ((priv->scan_periodic_next_msec - now_msec) / 1000),
+                       (int) ((priv->scan_periodic_next_msec - now_msec) % 1000),
+                       !priv->scan_kickoff_timeout_id ? ", schedule timeout" : "");
+            if (!priv->scan_kickoff_timeout_id) {
+                priv->scan_kickoff_timeout_id =
+                    g_timeout_add_seconds((priv->scan_periodic_next_msec - now_msec + 999) / 1000,
+                                          _scan_kickoff_timeout_cb,
+                                          self);
+            }
+            return;
+        }
+
+        priv->scan_periodic_interval_sec =
+            NM_CLAMP(((int) priv->scan_periodic_interval_sec) * 3 / 2,
+                     SCAN_INTERVAL_SEC_MIN,
+                     SCAN_INTERVAL_SEC_MAX);
+        priv->scan_periodic_next_msec = now_msec + 1000 * priv->scan_periodic_interval_sec;
+    }
+
+    ssids = _scan_request_ssids_build_hidden(self, now_msec, &has_hidden_profiles);
+    if (has_hidden_profiles) {
+        if (priv->hidden_probe_scan_warn) {
+            priv->hidden_probe_scan_warn = FALSE;
+            _LOGW(LOGD_WIFI,
+                  "wifi-scan: active scanning for networks due to profiles with wifi.hidden=yes. "
+                  "This makes you trackable");
+        }
+    } else if (!is_explict)
+        priv->hidden_probe_scan_warn = TRUE;
+
+    if (_LOGD_ENABLED(LOGD_WIFI)) {
+        gs_free char *ssids_str = NULL;
+        guint         ssids_len = 0;
+
+        if (ssids) {
+            gs_strfreev char **strv = NULL;
+            guint              i;
+
+            strv = g_new(char *, ssids->len + 1u);
+            for (i = 0; i < ssids->len; i++)
+                strv[i] = _nm_utils_ssid_to_string(ssids->pdata[i]);
+            strv[i] = NULL;
+
+            nm_assert(ssids->len > 0);
+            nm_assert(ssids->len == NM_PTRARRAY_LEN(strv));
+
+            ssids_str = g_strjoinv(", ", strv);
+            ssids_len = ssids->len;
+        }
+        _LOGD(LOGD_WIFI,
+              "wifi-scan: start %s scan (%u SSIDs to probe scan%s%s%s)",
+              is_explict ? "explicit" : "periodic",
+              ssids_len,
+              NM_PRINT_FMT_QUOTED(ssids_str, " [", ssids_str, "]", ""));
+    }
+
+    priv->scan_last_request_started_at_msec = now_msec;
+
+    if (is_explict)
+        _LOGT_scan("kickoff: explicit scan starting");
+    else {
+        _LOGT_scan("kickoff: periodic scan starting (next scan is scheduled in %d.%03d sec)",
+                   (int) ((priv->scan_periodic_next_msec - now_msec) / 1000),
+                   (int) ((priv->scan_periodic_next_msec - now_msec) % 1000));
+    }
+
+    _hw_addr_set_scanning(self, FALSE);
+
+    priv->scan_request_cancellable = g_cancellable_new();
+    nm_supplicant_interface_request_scan(priv->sup_iface,
+                                         ssids ? (GBytes *const *) ssids->pdata : NULL,
+                                         ssids ? ssids->len : 0u,
+                                         priv->scan_request_cancellable,
+                                         _scan_supplicant_request_scan_cb,
+                                         self);
+
+    /* It's OK to call _scan_notify_is_scanning() again. They mutually call each other,
+     * but _scan_kickoff() sets "priv->scan_request_cancellable" which will stop
+     * them from recursing indefinitely. */
+    _scan_notify_is_scanning(self);
+}
+
+/****************************************************************************
+ * WPA Supplicant control stuff
+ *
+ */
+
+static gboolean
+ap_list_dump(gpointer user_data)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(user_data);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    priv->ap_dump_id = 0;
+
+    if (_LOGD_ENABLED(LOGD_WIFI_SCAN)) {
+        NMWifiAP *ap;
+        gint64    now_msec = nm_utils_get_monotonic_timestamp_msec();
+        char      str_buf[100];
+
+        _LOGD(LOGD_WIFI_SCAN,
+              "APs: [now:%u.%03u, last:%s]",
+              (guint)(now_msec / NM_UTILS_MSEC_PER_SEC),
+              (guint)(now_msec % NM_UTILS_MSEC_PER_SEC),
+              priv->scan_last_complete_msec > 0
+                  ? nm_sprintf_buf(str_buf,
+                                   "%u.%03u",
+                                   (guint)(priv->scan_last_complete_msec / NM_UTILS_MSEC_PER_SEC),
+                                   (guint)(priv->scan_last_complete_msec % NM_UTILS_MSEC_PER_SEC))
+                  : "-1");
+        c_list_for_each_entry (ap, &priv->aps_lst_head, aps_lst)
+            _ap_dump(self, LOGL_DEBUG, ap, "dump", now_msec);
+    }
+    return G_SOURCE_REMOVE;
+}
+
+static void
+schedule_ap_list_dump(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (!priv->ap_dump_id && _LOGD_ENABLED(LOGD_WIFI_SCAN))
+        priv->ap_dump_id = g_timeout_add_seconds(1, ap_list_dump, self);
+}
+
+static void
+try_fill_ssid_for_hidden_ap(NMDeviceWifi *self, NMWifiAP *ap)
+{
+    const char *                 bssid;
+    NMSettingsConnection *const *connections;
+    guint                        i;
+
+    g_return_if_fail(nm_wifi_ap_get_ssid(ap) == NULL);
+
+    bssid = nm_wifi_ap_get_address(ap);
+    g_return_if_fail(bssid);
+
+    /* Look for this AP's BSSID in the seen-bssids list of a connection,
+     * and if a match is found, copy over the SSID */
+    connections = nm_settings_get_connections(nm_device_get_settings((NMDevice *) self), NULL);
+    for (i = 0; connections[i]; i++) {
+        NMSettingsConnection *sett_conn = connections[i];
+        NMSettingWireless *   s_wifi;
+
+        if (!nm_settings_connection_has_seen_bssid(sett_conn, bssid))
+            continue;
+        s_wifi =
+            nm_connection_get_setting_wireless(nm_settings_connection_get_connection(sett_conn));
+        if (!s_wifi)
+            continue;
+
+        nm_wifi_ap_set_ssid(ap, nm_setting_wireless_get_ssid(s_wifi));
+        break;
+    }
+}
+
+static void
+supplicant_iface_bss_changed_cb(NMSupplicantInterface *iface,
+                                NMSupplicantBssInfo *  bss_info,
+                                gboolean               is_present,
+                                NMDeviceWifi *         self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMWifiAP *           found_ap;
+    GBytes *             ssid;
+
+    found_ap = g_hash_table_lookup(priv->aps_idx_by_supplicant_path, bss_info->bss_path);
+
+    if (!is_present) {
+        if (!found_ap)
+            return;
+        if (found_ap == priv->current_ap) {
+            /* The current AP cannot be removed (to prevent NM indicating that
+             * it is connected, but to nothing), but it must be removed later
+             * when the current AP is changed or cleared.  Set 'fake' to
+             * indicate that this AP is now unknown to the supplicant.
+             */
+            if (nm_wifi_ap_set_fake(found_ap, TRUE))
+                _ap_dump(self, LOGL_DEBUG, found_ap, "updated", 0);
+        } else {
+            ap_add_remove(self, FALSE, found_ap, TRUE);
+            schedule_ap_list_dump(self);
+        }
+        return;
+    }
+
+    if (found_ap) {
+        if (!nm_wifi_ap_update_from_properties(found_ap, bss_info))
+            return;
+        _ap_dump(self, LOGL_DEBUG, found_ap, "updated", 0);
+    } else {
+        gs_unref_object NMWifiAP *ap = NULL;
+
+        if (!bss_info->bssid_valid) {
+            /* We failed to initialize the info about the AP. This can
+             * happen due to an error in the D-Bus communication. In this case
+             * we ignore the info. */
+            return;
+        }
+
+        ap = nm_wifi_ap_new_from_properties(bss_info);
+
+        /* Let the manager try to fill in the SSID from seen-bssids lists */
+        ssid = nm_wifi_ap_get_ssid(ap);
+        if (!ssid || _nm_utils_is_empty_ssid(ssid)) {
+            /* Try to fill the SSID from the AP database */
+            try_fill_ssid_for_hidden_ap(self, ap);
+
+            ssid = nm_wifi_ap_get_ssid(ap);
+            if (ssid && !_nm_utils_is_empty_ssid(ssid)) {
+                gs_free char *s = NULL;
+
+                /* Yay, matched it, no longer treat as hidden */
+                _LOGD(LOGD_WIFI,
+                      "matched hidden AP %s => %s",
+                      nm_wifi_ap_get_address(ap),
+                      (s = _nm_utils_ssid_to_string(ssid)));
+            } else {
+                /* Didn't have an entry for this AP in the database */
+                _LOGD(LOGD_WIFI, "failed to match hidden AP %s", nm_wifi_ap_get_address(ap));
+            }
+        }
+
+        ap_add_remove(self, TRUE, ap, TRUE);
+    }
+
+    /* Update the current AP if the supplicant notified a current BSS change
+     * before it sent the current BSS's scan result.
+     */
+    if (nm_supplicant_interface_get_current_bss(iface) == bss_info->bss_path)
+        supplicant_iface_notify_current_bss(priv->sup_iface, NULL, self);
+
+    schedule_ap_list_dump(self);
+}
+
+static void
+cleanup_association_attempt(NMDeviceWifi *self, gboolean disconnect)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->sup_timeout_id);
+    nm_clear_g_source(&priv->link_timeout_id);
+    nm_clear_g_source(&priv->wps_timeout_id);
+    if (disconnect && priv->sup_iface)
+        nm_supplicant_interface_disconnect(priv->sup_iface);
+}
+
+static void
+cleanup_supplicant_failures(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->reacquire_iface_id);
+    priv->failed_iface_count = 0;
+}
+
+static void
+wifi_secrets_cb(NMActRequest *                req,
+                NMActRequestGetSecretsCallId *call_id,
+                NMSettingsConnection *        connection,
+                GError *                      error,
+                gpointer                      user_data)
+{
+    NMDevice *           device = user_data;
+    NMDeviceWifi *       self   = user_data;
+    NMDeviceWifiPrivate *priv;
+
+    g_return_if_fail(NM_IS_DEVICE_WIFI(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->wifi_secrets_id == call_id);
+
+    priv->wifi_secrets_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    g_return_if_fail(req == nm_device_get_act_request(device));
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+    g_return_if_fail(nm_act_request_get_settings_connection(req) == connection);
+
+    if (error) {
+        _LOGW(LOGD_WIFI, "no secrets: %s", error->message);
+
+        /* Even if WPS is still pending, let's abort the activation when the secret
+         * request returns.
+         *
+         * This means, a user can only effectively use WPS when also running a secret
+         * agent, and pressing the push button while being prompted for the password.
+         * Note, that in the secret prompt the user can see that WPS is in progress
+         * (via the NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE flag).
+         *
+         * Previously, WPS was not cancelled when the secret request returns.
+         * Note that in common use-cases WPS is enabled in the connection profile
+         * but it won't succeed (because it's disabled in the AP or because the
+         * user is not prepared to press the push button).
+         * That means for example, during boot we would try to autoconnect with WPS.
+         * At that point, there is no secret-agent running, and WPS is pending for
+         * full 30 seconds. If in the meantime a secret agent registers (because
+         * of logging into the DE), the profile is still busy waiting for WPS to time
+         * out. Only after that delay, autoconnect starts again (note that autoconnect gets
+         * not blocked in this case, because a secret agent registered in the meantime).
+         *
+         * It seems wrong to continue doing WPS if the user is not aware
+         * that WPS is ongoing. The user is required to perform an action (push button),
+         * and must be told via the secret prompt.
+         * If no secret-agent is running, if the user cancels the secret-request, or any
+         * other error to obtain secrets, the user apparently does not want WPS either.
+         */
+        nm_clear_g_source(&priv->wps_timeout_id);
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+wifi_secrets_cancel(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    if (priv->wifi_secrets_id)
+        nm_act_request_cancel_secrets(NULL, priv->wifi_secrets_id);
+    nm_assert(!priv->wifi_secrets_id);
+}
+
+static void
+supplicant_iface_wps_credentials_cb(NMSupplicantInterface *iface,
+                                    GVariant *             credentials,
+                                    NMDeviceWifi *         self)
+{
+    NMActRequest *   req;
+    gs_unref_variant GVariant *val_key = NULL;
+    gs_unref_variant GVariant *secrets = NULL;
+    gs_free_error GError *error        = NULL;
+    const char *          array;
+    gsize                 psk_len = 0;
+
+    if (nm_device_get_state(NM_DEVICE(self)) != NM_DEVICE_STATE_NEED_AUTH) {
+        _LOGI(LOGD_DEVICE | LOGD_WIFI, "WPS: The connection can't be updated with credentials");
+        return;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI, "WPS: Updating the connection with credentials");
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    val_key = g_variant_lookup_value(credentials, "Key", G_VARIANT_TYPE_BYTESTRING);
+    if (val_key) {
+        char psk[64];
+
+        array = g_variant_get_fixed_array(val_key, &psk_len, 1);
+        if (psk_len >= 8 && psk_len <= 63) {
+            memcpy(psk, array, psk_len);
+            psk[psk_len] = '\0';
+            if (g_utf8_validate(psk, psk_len, NULL)) {
+                secrets = g_variant_new_parsed("[{%s, [{%s, <%s>}]}]",
+                                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                               NM_SETTING_WIRELESS_SECURITY_PSK,
+                                               psk);
+                g_variant_ref_sink(secrets);
+            }
+        }
+        if (!secrets)
+            _LOGW(LOGD_DEVICE | LOGD_WIFI, "WPS: ignore invalid PSK");
+    }
+
+    if (!secrets)
+        return;
+
+    if (!nm_settings_connection_new_secrets(nm_act_request_get_settings_connection(req),
+                                            nm_act_request_get_applied_connection(req),
+                                            NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                                            secrets,
+                                            &error)) {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "WPS: Could not update the connection with credentials: %s",
+              error->message);
+        return;
+    }
+
+    wifi_secrets_cancel(self);
+    nm_device_activate_schedule_stage1_device_prepare(NM_DEVICE(self), FALSE);
+}
+
+static gboolean
+wps_timeout_cb(gpointer user_data)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(user_data);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    priv->wps_timeout_id = 0;
+    if (!priv->wifi_secrets_id) {
+        /* Fail only if the secrets are not being requested. */
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_NO_SECRETS);
+    }
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+wifi_secrets_get_secrets(NMDeviceWifi *               self,
+                         const char *                 setting_name,
+                         NMSecretAgentGetSecretsFlags flags)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMActRequest *       req;
+
+    wifi_secrets_cancel(self);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(NM_IS_ACT_REQUEST(req));
+
+    priv->wifi_secrets_id =
+        nm_act_request_get_secrets(req, TRUE, setting_name, flags, NULL, wifi_secrets_cb, self);
+    g_return_if_fail(priv->wifi_secrets_id);
+}
+
+/*
+ * link_timeout_cb
+ *
+ * Called when the link to the access point has been down for a specified
+ * period of time.
+ */
+static gboolean
+link_timeout_cb(gpointer user_data)
+{
+    NMDevice *           device = NM_DEVICE(user_data);
+    NMDeviceWifi *       self   = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    _LOGW(LOGD_WIFI, "link timed out.");
+
+    priv->link_timeout_id = 0;
+
+    /* Disconnect event while activated; the supplicant hasn't been able
+     * to reassociate within the timeout period, so the connection must
+     * fail.
+     */
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_ACTIVATED)
+        return FALSE;
+
+    set_current_ap(self, NULL, TRUE);
+
+    nm_device_state_changed(device,
+                            NM_DEVICE_STATE_FAILED,
+                            priv->ssid_found ? NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT
+                                             : NM_DEVICE_STATE_REASON_SSID_NOT_FOUND);
+    return FALSE;
+}
+
+static gboolean
+need_new_8021x_secrets(NMDeviceWifi *             self,
+                       NMSupplicantInterfaceState old_state,
+                       const char **              setting_name)
+{
+    NMSetting8021x *           s_8021x;
+    NMSettingWirelessSecurity *s_wsec;
+    NMSettingSecretFlags       secret_flags = NM_SETTING_SECRET_FLAG_NONE;
+    NMConnection *             connection;
+
+    g_return_val_if_fail(setting_name, FALSE);
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    g_return_val_if_fail(connection != NULL, FALSE);
+
+    /* 802.1x stuff only happens in the supplicant's ASSOCIATED state when it's
+     * attempting to authenticate with the AP.
+     */
+    if (old_state != NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATED)
+        return FALSE;
+
+    /* If it's an 802.1x or LEAP connection with "always ask"/unsaved secrets
+     * then we need to ask again because it might be an OTP token and the PIN
+     * may have changed.
+     */
+
+    s_8021x = nm_connection_get_setting_802_1x(connection);
+    if (s_8021x) {
+        if (!nm_setting_get_secret_flags(NM_SETTING(s_8021x),
+                                         NM_SETTING_802_1X_PASSWORD,
+                                         &secret_flags,
+                                         NULL))
+            g_assert_not_reached();
+        if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED)
+            *setting_name = NM_SETTING_802_1X_SETTING_NAME;
+        return *setting_name ? TRUE : FALSE;
+    }
+
+    s_wsec = nm_connection_get_setting_wireless_security(connection);
+    if (s_wsec) {
+        if (!nm_setting_get_secret_flags(NM_SETTING(s_wsec),
+                                         NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD,
+                                         &secret_flags,
+                                         NULL))
+            g_assert_not_reached();
+        if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED)
+            *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+        return *setting_name ? TRUE : FALSE;
+    }
+
+    /* Not a LEAP or 802.1x connection */
+    return FALSE;
+}
+
+static gboolean
+need_new_wpa_psk(NMDeviceWifi *             self,
+                 NMSupplicantInterfaceState old_state,
+                 int                        disconnect_reason,
+                 const char **              setting_name)
+{
+    NMSettingWirelessSecurity *s_wsec;
+    NMConnection *             connection;
+    const char *               key_mgmt = NULL;
+
+    g_return_val_if_fail(setting_name, FALSE);
+
+    connection = nm_device_get_applied_connection(NM_DEVICE(self));
+
+    g_return_val_if_fail(connection, FALSE);
+
+    /* A bad PSK will cause the supplicant to disconnect during the 4-way handshake */
+    if (old_state != NM_SUPPLICANT_INTERFACE_STATE_4WAY_HANDSHAKE)
+        return FALSE;
+
+    s_wsec = nm_connection_get_setting_wireless_security(connection);
+    if (s_wsec)
+        key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+
+    if (g_strcmp0(key_mgmt, "wpa-psk") == 0) {
+/* -4 (locally-generated WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY) usually
+         * means the driver missed beacons from the AP.  This usually happens
+         * due to driver bugs or faulty power-save management.  It doesn't
+         * indicate that the PSK is wrong.
+         */
+#define LOCAL_WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY -4
+        if (disconnect_reason == LOCAL_WLAN_REASON_DISASSOC_DUE_TO_INACTIVITY)
+            return FALSE;
+
+        *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+        return TRUE;
+    }
+
+    /* Not a WPA-PSK connection */
+    return FALSE;
+}
+
+static gboolean
+handle_8021x_or_psk_auth_fail(NMDeviceWifi *             self,
+                              NMSupplicantInterfaceState new_state,
+                              NMSupplicantInterfaceState old_state,
+                              int                        disconnect_reason)
+{
+    NMDevice *    device = NM_DEVICE(self);
+    NMActRequest *req;
+    const char *  setting_name = NULL;
+    gboolean      handled      = FALSE;
+
+    g_return_val_if_fail(new_state == NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, FALSE);
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_val_if_fail(req != NULL, FALSE);
+
+    if (need_new_8021x_secrets(self, old_state, &setting_name)
+        || need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) {
+        nm_act_request_clear_secrets(req);
+
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) disconnected during association, asking for new key");
+
+        cleanup_association_attempt(self, TRUE);
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_NEED_AUTH,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+        wifi_secrets_get_secrets(self,
+                                 setting_name,
+                                 NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+                                     | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
+        handled = TRUE;
+    }
+
+    return handled;
+}
+
+static gboolean
+reacquire_interface_cb(gpointer user_data)
+{
+    NMDevice *           device = NM_DEVICE(user_data);
+    NMDeviceWifi *       self   = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    priv->reacquire_iface_id = 0;
+    priv->failed_iface_count++;
+
+    _LOGW(LOGD_WIFI, "re-acquiring supplicant interface (#%d).", priv->failed_iface_count);
+
+    if (!priv->sup_iface)
+        supplicant_interface_acquire(self);
+
+    return G_SOURCE_REMOVE;
+}
+
+static void
+supplicant_iface_state_down(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+
+    nm_device_queue_recheck_available(device,
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                      NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+    cleanup_association_attempt(self, FALSE);
+
+    /* If the device is already in UNAVAILABLE state then the state change
+     * is a NOP and the interface won't be re-acquired in the device state
+     * change handler.  So ensure we have a new one here so that we're
+     * ready if the supplicant comes back.
+     */
+    supplicant_interface_release(self);
+    if (priv->failed_iface_count < 5)
+        priv->reacquire_iface_id = g_timeout_add_seconds(10, reacquire_interface_cb, self);
+    else
+        _LOGI(LOGD_DEVICE | LOGD_WIFI, "supplicant interface keeps failing, giving up");
+}
+
+static void
+supplicant_iface_state(NMDeviceWifi *             self,
+                       NMSupplicantInterfaceState new_state,
+                       NMSupplicantInterfaceState old_state,
+                       int                        disconnect_reason,
+                       gboolean                   is_real_signal)
+{
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMDevice *           device = NM_DEVICE(self);
+    NMDeviceState        devstate;
+    gboolean             scanning;
+    gboolean             scan_changed;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "supplicant interface state: %s -> %s%s",
+          nm_supplicant_interface_state_to_string(old_state),
+          nm_supplicant_interface_state_to_string(new_state),
+          is_real_signal ? "" : " (simulated signal)");
+
+    if (new_state == NM_SUPPLICANT_INTERFACE_STATE_DOWN) {
+        supplicant_iface_state_down(self);
+        goto out;
+    }
+
+    devstate = nm_device_get_state(device);
+    scanning = nm_supplicant_interface_get_scanning(priv->sup_iface);
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING) {
+        _LOGD(LOGD_WIFI, "supplicant ready");
+        nm_device_queue_recheck_available(NM_DEVICE(device),
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE,
+                                          NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        priv->scan_periodic_interval_sec = 0;
+        priv->scan_periodic_next_msec    = 0;
+    }
+
+    /* In these states we know the supplicant is actually talking to something */
+    if (new_state >= NM_SUPPLICANT_INTERFACE_STATE_ASSOCIATING
+        && new_state <= NM_SUPPLICANT_INTERFACE_STATE_COMPLETED)
+        priv->ssid_found = TRUE;
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING)
+        recheck_p2p_availability(self);
+
+    switch (new_state) {
+    case NM_SUPPLICANT_INTERFACE_STATE_COMPLETED:
+        nm_clear_g_source(&priv->sup_timeout_id);
+        nm_clear_g_source(&priv->link_timeout_id);
+        nm_clear_g_source(&priv->wps_timeout_id);
+
+        /* If this is the initial association during device activation,
+         * schedule the next activation stage.
+         */
+        if (devstate == NM_DEVICE_STATE_CONFIG) {
+            NMSettingWireless *s_wifi;
+            GBytes *           ssid;
+            gs_free char *     ssid_str = NULL;
+
+            s_wifi = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_WIRELESS);
+
+            g_return_if_fail(s_wifi);
+
+            ssid = nm_setting_wireless_get_ssid(s_wifi);
+            g_return_if_fail(ssid);
+
+            _LOGI(LOGD_DEVICE | LOGD_WIFI,
+                  "Activation: (wifi) Stage 2 of 5 (Device Configure) successful. %s %s",
+                  priv->mode == NM_802_11_MODE_AP ? "Started Wi-Fi Hotspot"
+                                                  : "Connected to wireless network",
+                  (ssid_str = _nm_utils_ssid_to_string(ssid)));
+            nm_device_activate_schedule_stage3_ip_config_start(device);
+        } else if (devstate == NM_DEVICE_STATE_ACTIVATED)
+            periodic_update(self);
+        break;
+    case NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED:
+        if ((devstate == NM_DEVICE_STATE_ACTIVATED) || nm_device_is_activating(device)) {
+            /* Disconnect of an 802.1x/LEAP connection during authentication,
+             * or disconnect of a WPA-PSK connection during the 4-way handshake,
+             * often means secrets are wrong. Not always the case, but until we
+             * have more information from wpa_supplicant about why the
+             * disconnect happened this is the best we can do.
+             */
+            if (handle_8021x_or_psk_auth_fail(self, new_state, old_state, disconnect_reason))
+                break;
+        }
+
+        /* Otherwise, it might be a stupid driver or some transient error, so
+         * let the supplicant try to reconnect a few more times.  Give it more
+         * time if a scan is in progress since the link might be dropped during
+         * the scan but will be re-established when the scan is done.
+         */
+        if (devstate == NM_DEVICE_STATE_ACTIVATED) {
+            if (priv->link_timeout_id == 0) {
+                priv->link_timeout_id =
+                    g_timeout_add_seconds(scanning ? 30 : 15, link_timeout_cb, self);
+                priv->ssid_found = FALSE;
+            }
+        }
+        break;
+    case NM_SUPPLICANT_INTERFACE_STATE_INACTIVE:
+        /* we would clear _scan_has_pending_action_set() and trigger a new scan.
+         * However, we don't want to cancel the current pending action, so force
+         * a new scan request. */
+        break;
+    default:
+        break;
+    }
+
+out:
+    scan_changed = _scan_notify_allowed(self, NM_TERNARY_FALSE);
+    scan_changed |= _scan_notify_is_scanning(self);
+    if (scan_changed)
+        _scan_kickoff(self);
+
+    if (old_state == NM_SUPPLICANT_INTERFACE_STATE_STARTING)
+        nm_device_remove_pending_action(device, NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT, TRUE);
+}
+
+static void
+supplicant_iface_state_cb(NMSupplicantInterface *iface,
+                          int                    new_state_i,
+                          int                    old_state_i,
+                          int                    disconnect_reason,
+                          gpointer               user_data)
+{
+    supplicant_iface_state(user_data, new_state_i, old_state_i, disconnect_reason, TRUE);
+}
+
+static void
+supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer user_data)
+{
+    NMDeviceWifi *self   = NM_DEVICE_WIFI(user_data);
+    NMDevice *    device = NM_DEVICE(self);
+
+    if (error && !nm_utils_error_is_cancelled_or_disposing(error)
+        && nm_device_is_activating(device)) {
+        cleanup_association_attempt(self, TRUE);
+        nm_device_queue_state(device,
+                              NM_DEVICE_STATE_FAILED,
+                              NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+    }
+}
+
+static void
+supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
+                                    GParamSpec *           pspec,
+                                    NMDeviceWifi *         self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMRefString *        current_bss;
+    NMWifiAP *           new_ap = NULL;
+
+    current_bss = nm_supplicant_interface_get_current_bss(iface);
+    if (current_bss)
+        new_ap = g_hash_table_lookup(priv->aps_idx_by_supplicant_path, current_bss);
+
+    if (new_ap != priv->current_ap) {
+        const char *  new_bssid  = NULL;
+        GBytes *      new_ssid   = NULL;
+        const char *  old_bssid  = NULL;
+        GBytes *      old_ssid   = NULL;
+        gs_free char *new_ssid_s = NULL;
+        gs_free char *old_ssid_s = NULL;
+
+        /* Don't ever replace a "fake" current AP if we don't know about the
+         * supplicant's current BSS yet.  It'll get replaced when we receive
+         * the current BSS's scan result.
+         */
+        if (new_ap == NULL && nm_wifi_ap_get_fake(priv->current_ap))
+            return;
+
+        if (new_ap) {
+            new_bssid = nm_wifi_ap_get_address(new_ap);
+            new_ssid  = nm_wifi_ap_get_ssid(new_ap);
+        }
+
+        if (priv->current_ap) {
+            old_bssid = nm_wifi_ap_get_address(priv->current_ap);
+            old_ssid  = nm_wifi_ap_get_ssid(priv->current_ap);
+        }
+
+        _LOGD(LOGD_WIFI,
+              "roamed from BSSID %s (%s) to %s (%s)",
+              old_bssid ?: "(none)",
+              (old_ssid_s = _nm_utils_ssid_to_string(old_ssid)),
+              new_bssid ?: "(none)",
+              (new_ssid_s = _nm_utils_ssid_to_string(new_ssid)));
+
+        if (new_bssid) {
+            /* The new AP could be in a different layer 3 network
+             * and so the old DHCP lease could be no longer valid.
+             * Also, some APs (e.g. Cisco) can be configured to drop
+             * all traffic until DHCP completes. To support such
+             * cases, renew the lease when roaming to a new AP. */
+            nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+        }
+
+        set_current_ap(self, new_ap, TRUE);
+    }
+}
+
+/* We bind the existence of the P2P device to a wifi device that is being
+ * managed by NetworkManager and is capable of P2P operation.
+ * Note that some care must be taken here, because we don't want to re-create
+ * the device every time the supplicant interface is destroyed (e.g. due to
+ * a suspend/resume cycle).
+ * Therefore, this function will be called when a change in the P2P capability
+ * is detected and the supplicant interface has been initialised.
+ */
+static void
+recheck_p2p_availability(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gboolean             p2p_available;
+
+    g_object_get(priv->sup_iface, NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE, &p2p_available, NULL);
+
+    if (p2p_available && !priv->p2p_device) {
+        gs_free char *iface_name = NULL;
+
+        /* Create a P2P device. "p2p-dev-" is the same prefix as chosen by
+         * wpa_supplicant internally.
+         */
+        iface_name = g_strconcat("p2p-dev-", nm_device_get_iface(NM_DEVICE(self)), NULL);
+
+        priv->p2p_device = nm_device_wifi_p2p_new(iface_name);
+
+        nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, priv->sup_iface);
+
+        g_signal_emit(self, signals[P2P_DEVICE_CREATED], 0, priv->p2p_device);
+        g_object_add_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device);
+        g_object_unref(priv->p2p_device);
+        return;
+    }
+
+    if (p2p_available && priv->p2p_device) {
+        nm_device_wifi_p2p_set_mgmt_iface(priv->p2p_device, priv->sup_iface);
+        return;
+    }
+
+    if (!p2p_available && priv->p2p_device) {
+        /* Destroy the P2P device. */
+        g_object_remove_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device);
+        nm_device_wifi_p2p_remove(g_steal_pointer(&priv->p2p_device));
+        return;
+    }
+}
+
+static void
+supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface,
+                                      GParamSpec *           pspec,
+                                      NMDeviceWifi *         self)
+{
+    if (nm_supplicant_interface_get_state(iface) > NM_SUPPLICANT_INTERFACE_STATE_STARTING)
+        recheck_p2p_availability(self);
+}
+
+static gboolean
+handle_auth_or_fail(NMDeviceWifi *self, NMActRequest *req, gboolean new_secrets)
+{
+    NMDeviceWifiPrivate *              priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    const char *                       setting_name;
+    NMConnection *                     applied_connection;
+    NMSettingWirelessSecurity *        s_wsec;
+    const char *                       bssid = NULL;
+    NM80211ApFlags                     ap_flags;
+    NMSettingWirelessSecurityWpsMethod wps_method;
+    const char *                       type;
+    NMSecretAgentGetSecretsFlags       get_secret_flags =
+        NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+
+    g_return_val_if_fail(NM_IS_DEVICE_WIFI(self), FALSE);
+
+    if (!req) {
+        req = nm_device_get_act_request(NM_DEVICE(self));
+        g_return_val_if_fail(req, FALSE);
+    }
+
+    if (!nm_device_auth_retries_try_next(NM_DEVICE(self)))
+        return FALSE;
+
+    nm_device_state_changed(NM_DEVICE(self),
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_NONE);
+
+    applied_connection = nm_act_request_get_applied_connection(req);
+    s_wsec             = nm_connection_get_setting_wireless_security(applied_connection);
+    wps_method         = nm_setting_wireless_security_get_wps_method(s_wsec);
+
+    /* Negotiate the WPS method */
+    if (wps_method == NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_DEFAULT)
+        wps_method = NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO;
+
+    if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO && priv->current_ap) {
+        /* Determine the method to use from AP capabilities. */
+        ap_flags = nm_wifi_ap_get_flags(priv->current_ap);
+        if (ap_flags & NM_802_11_AP_FLAGS_WPS_PBC)
+            wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC;
+        if (ap_flags & NM_802_11_AP_FLAGS_WPS_PIN)
+            wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN;
+        if (ap_flags & NM_802_11_AP_FLAGS_WPS
+            && wps_method == NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_AUTO) {
+            /* The AP doesn't specify which methods are supported. Allow all. */
+            wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC;
+            wps_method |= NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN;
+        }
+    }
+
+    if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PBC) {
+        get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_WPS_PBC_ACTIVE;
+        type = "pbc";
+    } else if (wps_method & NM_SETTING_WIRELESS_SECURITY_WPS_METHOD_PIN) {
+        type = "pin";
+    } else
+        type = NULL;
+
+    if (type) {
+        priv->wps_timeout_id = g_timeout_add_seconds(30, wps_timeout_cb, self);
+        if (priv->current_ap)
+            bssid = nm_wifi_ap_get_address(priv->current_ap);
+        nm_supplicant_interface_enroll_wps(priv->sup_iface, type, bssid, NULL);
+    }
+
+    nm_act_request_clear_secrets(req);
+    setting_name = nm_connection_need_secrets(applied_connection, NULL);
+    if (!setting_name) {
+        _LOGW(LOGD_DEVICE, "Cleared secrets, but setting didn't need any secrets.");
+        return FALSE;
+    }
+
+    if (new_secrets)
+        get_secret_flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+    wifi_secrets_get_secrets(self, setting_name, get_secret_flags);
+    return TRUE;
+}
+
+/*
+ * supplicant_connection_timeout_cb
+ *
+ * Called when the supplicant has been unable to connect to an access point
+ * within a specified period of time.
+ */
+static gboolean
+supplicant_connection_timeout_cb(gpointer user_data)
+{
+    NMDevice *           device = NM_DEVICE(user_data);
+    NMDeviceWifi *       self   = NM_DEVICE_WIFI(user_data);
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMActRequest *       req;
+    NMConnection *       connection;
+
+    cleanup_association_attempt(self, TRUE);
+
+    if (!nm_device_is_activating(device))
+        return FALSE;
+
+    /* Timed out waiting for a successful connection to the AP; if the AP's
+     * security requires network-side authentication (like WPA or 802.1x)
+     * and the connection attempt timed out then it's likely the authentication
+     * information (passwords, pin codes, etc) are wrong.
+     */
+
+    req = nm_device_get_act_request(device);
+    g_assert(req);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_assert(connection);
+
+    if (NM_IN_SET(priv->mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_MESH, NM_802_11_MODE_AP)) {
+        /* In Ad-Hoc and AP modes there's nothing to check the encryption key
+         * (if any), so supplicant timeouts here are almost certainly the wifi
+         * driver being really stupid.
+         */
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) %s network creation took too long, failing activation",
+              priv->mode == NM_802_11_MODE_ADHOC ? "Ad-Hoc" : "Hotspot");
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT);
+        return FALSE;
+    }
+
+    g_assert(priv->mode == NM_802_11_MODE_INFRA);
+
+    if (priv->ssid_found && nm_connection_get_setting_wireless_security(connection)) {
+        guint64  timestamp   = 0;
+        gboolean new_secrets = TRUE;
+
+        /* Connection failed; either driver problems, the encryption key is
+         * wrong, or the passwords or certificates were wrong.
+         */
+        _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) association took too long");
+
+        /* Ask for new secrets only if we've never activated this connection
+         * before.  If we've connected before, don't bother the user with
+         * dialogs, just retry or fail, and if we never connect the user can
+         * fix the password somewhere else.
+         */
+        if (nm_settings_connection_get_timestamp(nm_act_request_get_settings_connection(req),
+                                                 &timestamp))
+            new_secrets = !timestamp;
+
+        if (handle_auth_or_fail(self, req, new_secrets))
+            _LOGW(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) asking for new secrets");
+        else {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_NO_SECRETS);
+        }
+    } else {
+        _LOGW(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) association took too long, failing activation");
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                priv->ssid_found ? NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT
+                                                 : NM_DEVICE_STATE_REASON_SSID_NOT_FOUND);
+    }
+
+    return FALSE;
+}
+
+static NMSupplicantConfig *
+build_supplicant_config(NMDeviceWifi *self,
+                        NMConnection *connection,
+                        guint32       fixed_freq,
+                        GError **     error)
+{
+    NMDeviceWifiPrivate *         priv   = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSupplicantConfig *          config = NULL;
+    NMSettingWireless *           s_wireless;
+    NMSettingWirelessSecurity *   s_wireless_sec;
+    NMSettingWirelessSecurityPmf  pmf;
+    NMSettingWirelessSecurityFils fils;
+    NMTernary                     ap_isolation;
+
+    g_return_val_if_fail(priv->sup_iface, NULL);
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wireless != NULL, NULL);
+
+    config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface));
+
+    /* Warn if AP mode may not be supported */
+    if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP)
+        && nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_AP)
+               != NM_TERNARY_TRUE) {
+        _LOGW(LOGD_WIFI, "Supplicant may not support AP mode; connection may time out.");
+    }
+
+    if (!nm_supplicant_config_add_setting_wireless(config, s_wireless, fixed_freq, error)) {
+        g_prefix_error(error, "802-11-wireless: ");
+        goto error;
+    }
+
+    if (!nm_supplicant_config_add_bgscan(config, connection, error)) {
+        g_prefix_error(error, "bgscan: ");
+        goto error;
+    }
+
+    ap_isolation = nm_setting_wireless_get_ap_isolation(s_wireless);
+    if (ap_isolation == NM_TERNARY_DEFAULT) {
+        ap_isolation = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                                   "wifi.ap-isolation",
+                                                                   NM_DEVICE(self),
+                                                                   NM_TERNARY_FALSE,
+                                                                   NM_TERNARY_TRUE,
+                                                                   NM_TERNARY_FALSE);
+    }
+    nm_supplicant_config_set_ap_isolation(config, ap_isolation == NM_TERNARY_TRUE);
+
+    s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+    if (s_wireless_sec) {
+        NMSetting8021x *s_8021x;
+        const char *    con_uuid = nm_connection_get_uuid(connection);
+        guint32         mtu      = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
+                                               nm_device_get_ifindex(NM_DEVICE(self)));
+
+        g_assert(con_uuid);
+
+        /* Configure PMF (802.11w) */
+        pmf = nm_setting_wireless_security_get_pmf(s_wireless_sec);
+        if (pmf == NM_SETTING_WIRELESS_SECURITY_PMF_DEFAULT) {
+            pmf = nm_config_data_get_connection_default_int64(
+                NM_CONFIG_GET_DATA,
+                "wifi-sec.pmf",
+                NM_DEVICE(self),
+                NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE,
+                NM_SETTING_WIRELESS_SECURITY_PMF_REQUIRED,
+                NM_SETTING_WIRELESS_SECURITY_PMF_OPTIONAL);
+        }
+
+        /* Configure FILS (802.11ai) */
+        fils = nm_setting_wireless_security_get_fils(s_wireless_sec);
+        if (fils == NM_SETTING_WIRELESS_SECURITY_FILS_DEFAULT) {
+            fils = nm_config_data_get_connection_default_int64(
+                NM_CONFIG_GET_DATA,
+                "wifi-sec.fils",
+                NM_DEVICE(self),
+                NM_SETTING_WIRELESS_SECURITY_FILS_DISABLE,
+                NM_SETTING_WIRELESS_SECURITY_FILS_REQUIRED,
+                NM_SETTING_WIRELESS_SECURITY_FILS_OPTIONAL);
+        }
+
+        s_8021x = nm_connection_get_setting_802_1x(connection);
+        if (!nm_supplicant_config_add_setting_wireless_security(config,
+                                                                s_wireless_sec,
+                                                                s_8021x,
+                                                                con_uuid,
+                                                                mtu,
+                                                                pmf,
+                                                                fils,
+                                                                error)) {
+            g_prefix_error(error, "802-11-wireless-security: ");
+            goto error;
+        }
+    } else {
+        if (!nm_supplicant_config_add_no_security(config, error)) {
+            g_prefix_error(error, "unsecured-option: ");
+            goto error;
+        }
+    }
+
+    return config;
+
+error:
+    g_object_unref(config);
+    return NULL;
+}
+
+/*****************************************************************************/
+
+static gboolean
+wake_on_wlan_enable(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *       priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMSettingWirelessWakeOnWLan wowl;
+    NMSettingWireless *         s_wireless;
+
+    s_wireless = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_WIRELESS);
+    if (s_wireless) {
+        wowl = nm_setting_wireless_get_wake_on_wlan(s_wireless);
+        if (wowl != NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT)
+            goto found;
+    }
+
+    wowl = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       "wifi.wake-on-wlan",
+                                                       NM_DEVICE(self),
+                                                       NM_SETTING_WIRELESS_WAKE_ON_WLAN_NONE,
+                                                       G_MAXINT32,
+                                                       NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT);
+
+    if (NM_FLAGS_ANY(wowl, NM_SETTING_WIRELESS_WAKE_ON_WLAN_EXCLUSIVE_FLAGS)) {
+        if (!nm_utils_is_power_of_two(wowl)) {
+            _LOGD(LOGD_WIFI,
+                  "invalid default value %u for wake-on-wlan: "
+                  "'default' and 'ignore' are exclusive flags",
+                  (guint) wowl);
+            wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT;
+        }
+    } else if (NM_FLAGS_ANY(wowl, ~NM_SETTING_WIRELESS_WAKE_ON_WLAN_ALL)) {
+        _LOGD(LOGD_WIFI, "invalid default value %u for wake-on-wlan", (guint) wowl);
+        wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT;
+    }
+    if (wowl != NM_SETTING_WIRELESS_WAKE_ON_WLAN_DEFAULT)
+        goto found;
+
+    wowl = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE;
+found:
+    if (wowl == NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE) {
+        priv->wowlan_restore = wowl;
+        return TRUE;
+    }
+
+    priv->wowlan_restore =
+        nm_platform_wifi_get_wake_on_wlan(NM_PLATFORM_GET, nm_device_get_ifindex(NM_DEVICE(self)));
+
+    return nm_platform_wifi_set_wake_on_wlan(NM_PLATFORM_GET,
+                                             nm_device_get_ifindex(NM_DEVICE(self)),
+                                             wowl);
+}
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWifi *       self         = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv         = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMWifiAP *           ap           = NULL;
+    gs_unref_object NMWifiAP *ap_fake = NULL;
+    NMActRequest *            req;
+    NMConnection *            connection;
+    NMSettingWireless *       s_wireless;
+    const char *              mode;
+    const char *              ap_path;
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wireless, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_supplicant_interface_cancel_wps(priv->sup_iface);
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_INFRA) == 0)
+        priv->mode = NM_802_11_MODE_INFRA;
+    else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_ADHOC) == 0)
+        priv->mode = NM_802_11_MODE_ADHOC;
+    else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_AP) == 0) {
+        priv->mode = NM_802_11_MODE_AP;
+
+        /* Scanning not done in AP mode; clear the scan list */
+        remove_all_aps(self);
+    } else if (g_strcmp0(mode, NM_SETTING_WIRELESS_MODE_MESH) == 0)
+        priv->mode = NM_802_11_MODE_MESH;
+    _notify(self, PROP_MODE);
+
+    /* expire the temporary MAC address used during scanning */
+    priv->hw_addr_scan_expire = 0;
+
+    /* Set spoof MAC to the interface */
+    if (!nm_device_hw_addr_set_cloned(device, connection, TRUE)) {
+        *out_failure_reason = NM_DEVICE_STATE_REASON_CONFIG_FAILED;
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    /* AP and Mesh modes never use a specific object or existing scanned AP */
+    if (!NM_IN_SET(priv->mode, NM_802_11_MODE_AP, NM_802_11_MODE_MESH)) {
+        ap_path = nm_active_connection_get_specific_object(NM_ACTIVE_CONNECTION(req));
+        ap      = ap_path ? nm_wifi_ap_lookup_for_device(NM_DEVICE(self), ap_path) : NULL;
+    }
+    if (!ap)
+        ap = nm_wifi_aps_find_first_compatible(&priv->aps_lst_head, connection);
+
+    if (!ap) {
+        /* If the user is trying to connect to an AP that NM doesn't yet know about
+         * (hidden network or something), starting a Hotspot or joining a Mesh,
+         * create a fake APfrom the security settings in the connection.  This "fake"
+         * AP gets used until the real one is found in the scan list (Ad-Hoc or Hidden),
+         * or until the device is deactivated (Hotspot).
+         */
+        ap_fake = nm_wifi_ap_new_fake_from_connection(connection);
+        if (!ap_fake)
+            g_return_val_if_reached(NM_ACT_STAGE_RETURN_FAILURE);
+
+        if (nm_wifi_ap_is_hotspot(ap_fake))
+            nm_wifi_ap_set_address(ap_fake, nm_device_get_hw_address(device));
+
+        g_object_freeze_notify(G_OBJECT(self));
+        ap_add_remove(self, TRUE, ap_fake, TRUE);
+        g_object_thaw_notify(G_OBJECT(self));
+        ap = ap_fake;
+    }
+
+    _scan_notify_allowed(self, NM_TERNARY_DEFAULT);
+
+    set_current_ap(self, ap, FALSE);
+    nm_active_connection_set_specific_object(NM_ACTIVE_CONNECTION(req),
+                                             nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)));
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static void
+ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP *ap)
+{
+    NMDevice *    device     = NM_DEVICE(self);
+    const char *  band       = nm_setting_wireless_get_band(s_wifi);
+    const guint32 a_freqs[]  = {5180, 5200, 5220, 5745, 5765, 5785, 5805, 0};
+    const guint32 bg_freqs[] = {2412, 2437, 2462, 2472, 0};
+    guint32       freq       = 0;
+
+    g_assert(ap);
+
+    if (nm_wifi_ap_get_freq(ap))
+        return;
+
+    if (g_strcmp0(band, "a") == 0)
+        freq = nm_platform_wifi_find_frequency(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               a_freqs);
+    else
+        freq = nm_platform_wifi_find_frequency(nm_device_get_platform(device),
+                                               nm_device_get_ifindex(device),
+                                               bg_freqs);
+
+    if (!freq)
+        freq = (g_strcmp0(band, "a") == 0) ? 5180 : 2462;
+
+    if (nm_wifi_ap_set_freq(ap, freq))
+        _ap_dump(self, LOGL_DEBUG, ap, "updated", 0);
+}
+
+static void
+set_powersave(NMDevice *device)
+{
+    NMDeviceWifi *             self = NM_DEVICE_WIFI(device);
+    NMSettingWireless *        s_wireless;
+    NMSettingWirelessPowersave val;
+
+    s_wireless = nm_device_get_applied_setting(device, NM_TYPE_SETTING_WIRELESS);
+
+    g_return_if_fail(s_wireless);
+
+    val = nm_setting_wireless_get_powersave(s_wireless);
+    if (val == NM_SETTING_WIRELESS_POWERSAVE_DEFAULT) {
+        val = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                          "wifi.powersave",
+                                                          device,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_IGNORE,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_ENABLE,
+                                                          NM_SETTING_WIRELESS_POWERSAVE_IGNORE);
+    }
+
+    _LOGT(LOGD_WIFI, "powersave is set to %u", (unsigned) val);
+
+    if (val == NM_SETTING_WIRELESS_POWERSAVE_IGNORE)
+        return;
+
+    nm_platform_wifi_set_powersave(nm_device_get_platform(device),
+                                   nm_device_get_ifindex(device),
+                                   val == NM_SETTING_WIRELESS_POWERSAVE_ENABLE);
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWifi *       self                  = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv                  = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gs_unref_object NMSupplicantConfig *config = NULL;
+    NM80211Mode                         ap_mode;
+    NMActRequest *                      req;
+    NMWifiAP *                          ap;
+    NMConnection *                      connection;
+    const char *                        setting_name;
+    NMSettingWireless *                 s_wireless;
+    GError *                            error = NULL;
+    guint                               timeout;
+    NMActRequest *                      request;
+    NMActiveConnection *                master_ac;
+    NMDevice *                          master;
+
+    nm_clear_g_source(&priv->sup_timeout_id);
+    nm_clear_g_source(&priv->link_timeout_id);
+    nm_clear_g_source(&priv->wps_timeout_id);
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    ap = priv->current_ap;
+    if (!ap) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED);
+        goto out_fail;
+    }
+
+    ap_mode = nm_wifi_ap_get_mode(ap);
+
+    connection = nm_act_request_get_applied_connection(req);
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    nm_assert(s_wireless);
+
+    /* If we need secrets, get them */
+    setting_name = nm_connection_need_secrets(connection, NULL);
+    if (setting_name) {
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) access point '%s' has security, but secrets are required.",
+              nm_connection_get_id(connection));
+
+        if (!handle_auth_or_fail(self, req, FALSE)) {
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+            goto out_fail;
+        }
+
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+    }
+
+    if (!wake_on_wlan_enable(self))
+        _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot configure WoWLAN.");
+
+    /* have secrets, or no secrets required */
+    if (nm_connection_get_setting_wireless_security(connection)) {
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) connection '%s' has security, and secrets exist.  No new secrets "
+              "needed.",
+              nm_connection_get_id(connection));
+    } else {
+        _LOGI(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) connection '%s' requires no security.  No secrets needed.",
+              nm_connection_get_id(connection));
+    }
+
+    priv->ssid_found = FALSE;
+
+    /* Supplicant requires an initial frequency for Ad-Hoc, Hotspot and Mesh;
+     * if the user didn't specify one and we didn't find an AP that matched
+     * the connection, just pick a frequency the device supports.
+     */
+    if (NM_IN_SET(ap_mode, NM_802_11_MODE_ADHOC, NM_802_11_MODE_MESH) || nm_wifi_ap_is_hotspot(ap))
+        ensure_hotspot_frequency(self, s_wireless, ap);
+
+    if (ap_mode == NM_802_11_MODE_INFRA)
+        set_powersave(device);
+
+    /* Build up the supplicant configuration */
+    config = build_supplicant_config(self, connection, nm_wifi_ap_get_freq(ap), &error);
+    if (!config) {
+        _LOGE(LOGD_DEVICE | LOGD_WIFI,
+              "Activation: (wifi) couldn't build wireless configuration: %s",
+              error->message);
+        g_clear_error(&error);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED);
+        goto out_fail;
+    }
+
+    /* Tell the supplicant in which bridge the interface is */
+    if ((request = nm_device_get_act_request(device))
+        && (master_ac = nm_active_connection_get_master(NM_ACTIVE_CONNECTION(request)))
+        && (master = nm_active_connection_get_device(master_ac))
+        && nm_device_get_device_type(master) == NM_DEVICE_TYPE_BRIDGE) {
+        nm_supplicant_interface_set_bridge(priv->sup_iface, nm_device_get_iface(master));
+    } else
+        nm_supplicant_interface_set_bridge(priv->sup_iface, NULL);
+
+    nm_supplicant_interface_assoc(priv->sup_iface, config, supplicant_iface_assoc_cb, self);
+
+    /* Set up a timeout on the association attempt */
+    timeout              = nm_device_get_supplicant_timeout(NM_DEVICE(self));
+    priv->sup_timeout_id = g_timeout_add_seconds(timeout, supplicant_connection_timeout_cb, self);
+
+    if (!priv->periodic_update_id)
+        priv->periodic_update_id = g_timeout_add_seconds(6, periodic_update_cb, self);
+
+    /* We'll get stage3 started when the supplicant connects */
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+
+out_fail:
+    cleanup_association_attempt(self, TRUE);
+    wake_on_wlan_restore(self);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    gboolean      indicate_addressing_running;
+    NMConnection *connection;
+    const char *  method;
+
+    connection = nm_device_get_applied_connection(device);
+
+    method = nm_utils_get_ip_config_method(connection, addr_family);
+    if (addr_family == AF_INET)
+        indicate_addressing_running = NM_IN_STRSET(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO);
+    else {
+        indicate_addressing_running = NM_IN_STRSET(method,
+                                                   NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                                                   NM_SETTING_IP6_CONFIG_METHOD_DHCP);
+    }
+
+    if (indicate_addressing_running)
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     nm_device_get_ip_ifindex(device),
+                                                     TRUE);
+
+    return NM_DEVICE_CLASS(nm_device_wifi_parent_class)
+        ->act_stage3_ip_config_start(device, addr_family, out_config, out_failure_reason);
+}
+
+static guint32
+get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    return nm_device_get_configured_mtu_from_connection(device,
+                                                        NM_TYPE_SETTING_WIRELESS,
+                                                        out_source);
+}
+
+static gboolean
+is_static_wep(NMConnection *connection)
+{
+    NMSettingWirelessSecurity *s_wsec;
+    const char *               str;
+
+    g_return_val_if_fail(connection != NULL, FALSE);
+
+    s_wsec = nm_connection_get_setting_wireless_security(connection);
+    if (!s_wsec)
+        return FALSE;
+
+    str = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    if (g_strcmp0(str, "none") != 0)
+        return FALSE;
+
+    str = nm_setting_wireless_security_get_auth_alg(s_wsec);
+    if (g_strcmp0(str, "leap") == 0)
+        return FALSE;
+
+    return TRUE;
+}
+
+static NMActStageReturn
+act_stage4_ip_config_timeout(NMDevice *           device,
+                             int                  addr_family,
+                             NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMConnection *       connection;
+    NMSettingIPConfig *  s_ip;
+    gboolean             may_fail;
+
+    connection = nm_device_get_applied_connection(device);
+    s_ip       = nm_connection_get_setting_ip_config(connection, addr_family);
+    may_fail   = nm_setting_ip_config_get_may_fail(s_ip);
+
+    if (priv->mode == NM_802_11_MODE_AP)
+        goto call_parent;
+
+    if (may_fail || !is_static_wep(connection)) {
+        /* Not static WEP or failure allowed; let superclass handle it */
+        goto call_parent;
+    }
+
+    /* If IP configuration times out and it's a static WEP connection, that
+     * usually means the WEP key is wrong.  WEP's Open System auth mode has
+     * no provision for figuring out if the WEP key is wrong, so you just have
+     * to wait for DHCP to fail to figure it out.  For all other Wi-Fi security
+     * types (open, WPA, 802.1x, etc) if the secrets/certs were wrong the
+     * connection would have failed before IP configuration.
+     *
+     * Activation failed, we must have bad encryption key */
+    _LOGW(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) could not get IP configuration for connection '%s'.",
+          nm_connection_get_id(connection));
+
+    if (!handle_auth_or_fail(self, NULL, TRUE)) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) asking for new secrets");
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+
+call_parent:
+    return NM_DEVICE_CLASS(nm_device_wifi_parent_class)
+        ->act_stage4_ip_config_timeout(device, addr_family, out_failure_reason);
+}
+
+static void
+activation_success_handler(NMDevice *device)
+{
+    NMDeviceWifi *       self    = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv    = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    int                  ifindex = nm_device_get_ifindex(device);
+    NMActRequest *       req;
+
+    req = nm_device_get_act_request(device);
+    g_assert(req);
+
+    /* Clear any critical protocol notification in the wifi stack */
+    nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device), ifindex, FALSE);
+
+    /* There should always be a current AP, either a fake one because we haven't
+     * seen a scan result for the activated AP yet, or a real one from the
+     * supplicant's scan list.
+     */
+    g_warn_if_fail(priv->current_ap);
+    if (priv->current_ap) {
+        if (nm_wifi_ap_get_fake(priv->current_ap)) {
+            gboolean    ap_changed   = FALSE;
+            gboolean    update_bssid = !nm_wifi_ap_get_address(priv->current_ap);
+            gboolean    update_rate  = !nm_wifi_ap_get_max_bitrate(priv->current_ap);
+            NMEtherAddr bssid;
+            guint32     rate;
+
+            /* If the activation AP hasn't been seen by the supplicant in a scan
+             * yet, it will be "fake".  This usually happens for Ad-Hoc and
+             * AP-mode connections.  Fill in the details from the device itself
+             * until the supplicant sends the scan result.
+             */
+            if (!nm_wifi_ap_get_freq(priv->current_ap))
+                ap_changed |= nm_wifi_ap_set_freq(
+                    priv->current_ap,
+                    nm_platform_wifi_get_frequency(nm_device_get_platform(device), ifindex));
+
+            if ((update_bssid || update_rate)
+                && nm_platform_wifi_get_station(nm_device_get_platform(device),
+                                                ifindex,
+                                                update_bssid ? &bssid : NULL,
+                                                NULL,
+                                                update_rate ? &rate : NULL)) {
+                if (update_bssid && nm_ether_addr_is_valid(&bssid))
+                    ap_changed |= nm_wifi_ap_set_address_bin(priv->current_ap, &bssid);
+                if (update_rate)
+                    ap_changed |= nm_wifi_ap_set_max_bitrate(priv->current_ap, rate);
+            }
+
+            if (ap_changed)
+                _ap_dump(self, LOGL_DEBUG, priv->current_ap, "updated", 0);
+        }
+
+        nm_active_connection_set_specific_object(
+            NM_ACTIVE_CONNECTION(req),
+            nm_dbus_object_get_path(NM_DBUS_OBJECT(priv->current_ap)));
+    }
+
+    periodic_update(self);
+
+    update_seen_bssids_cache(self, priv->current_ap);
+
+    priv->scan_periodic_interval_sec = 0;
+    priv->scan_periodic_next_msec    = 0;
+}
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceWifi *       self      = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv      = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    gboolean             clear_aps = FALSE;
+
+    if (new_state > NM_DEVICE_STATE_ACTIVATED)
+        wifi_secrets_cancel(self);
+
+    if (new_state <= NM_DEVICE_STATE_UNAVAILABLE) {
+        /* Clean up the supplicant interface because in these states the
+         * device cannot be used.
+         */
+        supplicant_interface_release(self);
+
+        nm_clear_g_source(&priv->periodic_update_id);
+
+        cleanup_association_attempt(self, TRUE);
+        cleanup_supplicant_failures(self);
+        remove_all_aps(self);
+    }
+
+    switch (new_state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+        clear_aps = TRUE;
+        break;
+    case NM_DEVICE_STATE_UNAVAILABLE:
+        /* If the device is enabled and the supplicant manager is ready,
+         * acquire a supplicant interface and transition to DISCONNECTED because
+         * the device is now ready to use.
+         */
+        if (priv->enabled && (nm_device_get_firmware_missing(device) == FALSE)) {
+            if (!priv->sup_iface)
+                supplicant_interface_acquire(self);
+        }
+        clear_aps = TRUE;
+        break;
+    case NM_DEVICE_STATE_NEED_AUTH:
+        if (priv->sup_iface)
+            nm_supplicant_interface_disconnect(priv->sup_iface);
+        break;
+    case NM_DEVICE_STATE_IP_CHECK:
+        /* Clear any critical protocol notification in the wifi stack */
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     nm_device_get_ifindex(device),
+                                                     FALSE);
+        break;
+    case NM_DEVICE_STATE_ACTIVATED:
+        activation_success_handler(device);
+        break;
+    case NM_DEVICE_STATE_FAILED:
+        /* Clear any critical protocol notification in the wifi stack */
+        nm_platform_wifi_indicate_addressing_running(nm_device_get_platform(device),
+                                                     nm_device_get_ifindex(device),
+                                                     FALSE);
+        break;
+    case NM_DEVICE_STATE_DISCONNECTED:
+        break;
+    default:
+        break;
+    }
+
+    if (clear_aps)
+        remove_all_aps(self);
+
+    _scan_notify_allowed(self, NM_TERNARY_DEFAULT);
+}
+
+static gboolean
+get_enabled(NMDevice *device)
+{
+    return NM_DEVICE_WIFI_GET_PRIVATE(device)->enabled;
+}
+
+static void
+set_enabled(NMDevice *device, gboolean enabled)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    NMDeviceState        state;
+
+    enabled = !!enabled;
+
+    if (priv->enabled == enabled)
+        return;
+
+    priv->enabled = enabled;
+
+    _LOGD(LOGD_WIFI, "device now %s", enabled ? "enabled" : "disabled");
+
+    state = nm_device_get_state(NM_DEVICE(self));
+    if (state < NM_DEVICE_STATE_UNAVAILABLE) {
+        _LOGD(LOGD_WIFI, "(%s): device blocked by UNMANAGED state", enabled ? "enable" : "disable");
+        return;
+    }
+
+    if (enabled) {
+        gboolean no_firmware = FALSE;
+
+        if (state != NM_DEVICE_STATE_UNAVAILABLE)
+            _LOGW(LOGD_CORE, "not in expected unavailable state!");
+
+        if (!nm_device_bring_up(NM_DEVICE(self), TRUE, &no_firmware)) {
+            _LOGD(LOGD_WIFI, "enable blocked by failure to bring device up");
+
+            if (no_firmware)
+                nm_device_set_firmware_missing(NM_DEVICE(device), TRUE);
+            else {
+                /* The device sucks, or the kernel was lying to us about the killswitch state */
+                priv->enabled = FALSE;
+            }
+            return;
+        }
+
+        /* Re-initialize the supplicant interface and wait for it to be ready */
+        cleanup_supplicant_failures(self);
+        supplicant_interface_release(self);
+        supplicant_interface_acquire(self);
+
+        _LOGD(LOGD_WIFI, "enable waiting on supplicant state");
+    } else {
+        nm_device_state_changed(NM_DEVICE(self),
+                                NM_DEVICE_STATE_UNAVAILABLE,
+                                NM_DEVICE_STATE_REASON_NONE);
+        nm_device_take_down(NM_DEVICE(self), TRUE);
+    }
+}
+
+static gboolean
+get_guessed_metered(NMDevice *device)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(device);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    return priv->current_ap && nm_wifi_ap_get_metered(priv->current_ap);
+}
+
+static gboolean
+can_reapply_change(NMDevice *  device,
+                   const char *setting_name,
+                   NMSetting * s_old,
+                   NMSetting * s_new,
+                   GHashTable *diffs,
+                   GError **   error)
+{
+    NMDeviceClass *device_class;
+
+    /* Only handle wireless setting here, delegate other settings to parent class */
+    if (nm_streq(setting_name, NM_SETTING_WIRELESS_SETTING_NAME)) {
+        return nm_device_hash_check_invalid_keys(
+            diffs,
+            NM_SETTING_WIRELESS_SETTING_NAME,
+            error,
+            NM_SETTING_WIRELESS_SEEN_BSSIDS, /* ignored */
+            NM_SETTING_WIRELESS_MTU,         /* reapplied with IP config */
+            NM_SETTING_WIRELESS_WAKE_ON_WLAN);
+    }
+
+    device_class = NM_DEVICE_CLASS(nm_device_wifi_parent_class);
+    return device_class->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+}
+
+static void
+reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_new)
+{
+    NMDeviceWifi *self  = NM_DEVICE_WIFI(device);
+    NMDeviceState state = nm_device_get_state(device);
+
+    NM_DEVICE_CLASS(nm_device_wifi_parent_class)->reapply_connection(device, con_old, con_new);
+
+    _LOGD(LOGD_DEVICE, "reapplying wireless settings");
+
+    if (state >= NM_DEVICE_STATE_CONFIG && !wake_on_wlan_enable(self))
+        _LOGW(LOGD_DEVICE | LOGD_WIFI, "Cannot configure WoWLAN.");
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(object);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+    const char **        list;
+
+    switch (prop_id) {
+    case PROP_MODE:
+        g_value_set_uint(value, priv->mode);
+        break;
+    case PROP_BITRATE:
+        g_value_set_uint(value, priv->rate);
+        break;
+    case PROP_CAPABILITIES:
+        g_value_set_uint(value, priv->capabilities);
+        break;
+    case PROP_ACCESS_POINTS:
+        list = nm_wifi_aps_get_paths(&priv->aps_lst_head, TRUE);
+        g_value_take_boxed(value, nm_utils_strv_make_deep_copied(list));
+        break;
+    case PROP_ACTIVE_ACCESS_POINT:
+        nm_dbus_utils_g_value_set_object_path(value, priv->current_ap);
+        break;
+    case PROP_SCANNING:
+        g_value_set_boolean(value, nm_device_wifi_get_scanning(self));
+        break;
+    case PROP_LAST_SCAN:
+        g_value_set_int64(
+            value,
+            priv->scan_last_complete_msec > 0
+                ? nm_utils_monotonic_timestamp_as_boottime(priv->scan_last_complete_msec,
+                                                           NM_UTILS_NSEC_PER_MSEC)
+                : (gint64) -1);
+        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)
+{
+    NMDeviceWifi *       device = NM_DEVICE_WIFI(object);
+    NMDeviceWifiPrivate *priv   = NM_DEVICE_WIFI_GET_PRIVATE(device);
+
+    switch (prop_id) {
+    case PROP_CAPABILITIES:
+        /* construct-only */
+        priv->capabilities = g_value_get_uint(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_wifi_init(NMDeviceWifi *self)
+{
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    c_list_init(&priv->aps_lst_head);
+    c_list_init(&priv->scanning_prohibited_lst_head);
+    c_list_init(&priv->scan_request_ssids_lst_head);
+    priv->aps_idx_by_supplicant_path = g_hash_table_new(nm_direct_hash, NULL);
+
+    priv->scan_last_request_started_at_msec = G_MININT64;
+    priv->hidden_probe_scan_warn            = TRUE;
+    priv->mode                              = NM_802_11_MODE_INFRA;
+    priv->wowlan_restore                    = NM_SETTING_WIRELESS_WAKE_ON_WLAN_IGNORE;
+}
+
+static void
+constructed(GObject *object)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(object);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    G_OBJECT_CLASS(nm_device_wifi_parent_class)->constructed(object);
+
+    if (priv->capabilities & NM_WIFI_DEVICE_CAP_AP)
+        _LOGI(LOGD_PLATFORM | LOGD_WIFI, "driver supports Access Point (AP) mode");
+
+    /* Connect to the supplicant manager */
+    priv->sup_mgr = g_object_ref(nm_supplicant_manager_get());
+}
+
+NMDevice *
+nm_device_wifi_new(const char *iface, NMDeviceWifiCapabilities capabilities)
+{
+    return g_object_new(NM_TYPE_DEVICE_WIFI,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "802.11 Wi-Fi",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_WIFI,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_WIFI,
+                        NM_DEVICE_RFKILL_TYPE,
+                        RFKILL_TYPE_WLAN,
+                        NM_DEVICE_WIFI_CAPABILITIES,
+                        (guint) capabilities,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(object);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_assert(c_list_is_empty(&priv->scanning_prohibited_lst_head));
+
+    nm_clear_g_source(&priv->periodic_update_id);
+
+    wifi_secrets_cancel(self);
+
+    cleanup_association_attempt(self, TRUE);
+    supplicant_interface_release(self);
+    cleanup_supplicant_failures(self);
+
+    g_clear_object(&priv->sup_mgr);
+
+    remove_all_aps(self);
+
+    if (priv->p2p_device) {
+        /* Destroy the P2P device. */
+        g_object_remove_weak_pointer(G_OBJECT(priv->p2p_device), (gpointer *) &priv->p2p_device);
+        nm_device_wifi_p2p_remove(g_steal_pointer(&priv->p2p_device));
+    }
+
+    G_OBJECT_CLASS(nm_device_wifi_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMDeviceWifi *       self = NM_DEVICE_WIFI(object);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    nm_assert(c_list_is_empty(&priv->aps_lst_head));
+    nm_assert(g_hash_table_size(priv->aps_idx_by_supplicant_path) == 0);
+
+    g_hash_table_unref(priv->aps_idx_by_supplicant_path);
+
+    G_OBJECT_CLASS(nm_device_wifi_parent_class)->finalize(object);
+}
+
+static void
+nm_device_wifi_class_init(NMDeviceWifiClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->constructed  = constructed;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    dbus_object_class->interface_infos =
+        NM_DBUS_INTERFACE_INFOS(&nm_interface_info_device_wireless);
+
+    device_class->connection_type_supported        = NM_SETTING_WIRELESS_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_WIRELESS_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_WIFI);
+
+    device_class->can_auto_connect            = can_auto_connect;
+    device_class->get_autoconnect_allowed     = get_autoconnect_allowed;
+    device_class->is_available                = is_available;
+    device_class->check_connection_compatible = check_connection_compatible;
+    device_class->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->get_enabled                 = get_enabled;
+    device_class->get_guessed_metered         = get_guessed_metered;
+    device_class->set_enabled                 = set_enabled;
+
+    device_class->act_stage1_prepare           = act_stage1_prepare;
+    device_class->act_stage2_config            = act_stage2_config;
+    device_class->get_configured_mtu           = get_configured_mtu;
+    device_class->act_stage3_ip_config_start   = act_stage3_ip_config_start;
+    device_class->act_stage4_ip_config_timeout = act_stage4_ip_config_timeout;
+    device_class->deactivate_async             = deactivate_async;
+    device_class->deactivate                   = deactivate;
+    device_class->deactivate_reset_hw_addr     = deactivate_reset_hw_addr;
+    device_class->unmanaged_on_quit            = unmanaged_on_quit;
+    device_class->can_reapply_change           = can_reapply_change;
+    device_class->reapply_connection           = reapply_connection;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_MODE] = g_param_spec_uint(NM_DEVICE_WIFI_MODE,
+                                                  "",
+                                                  "",
+                                                  NM_802_11_MODE_UNKNOWN,
+                                                  NM_802_11_MODE_AP,
+                                                  NM_802_11_MODE_INFRA,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_BITRATE] = g_param_spec_uint(NM_DEVICE_WIFI_BITRATE,
+                                                     "",
+                                                     "",
+                                                     0,
+                                                     G_MAXUINT32,
+                                                     0,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ACCESS_POINTS] =
+        g_param_spec_boxed(NM_DEVICE_WIFI_ACCESS_POINTS,
+                           "",
+                           "",
+                           G_TYPE_STRV,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_ACTIVE_ACCESS_POINT] =
+        g_param_spec_string(NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_WIFI_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_WIFI_DEVICE_CAP_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SCANNING] = g_param_spec_boolean(NM_DEVICE_WIFI_SCANNING,
+                                                         "",
+                                                         "",
+                                                         FALSE,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LAST_SCAN] = g_param_spec_int64(NM_DEVICE_WIFI_LAST_SCAN,
+                                                        "",
+                                                        "",
+                                                        -1,
+                                                        G_MAXINT64,
+                                                        -1,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[P2P_DEVICE_CREATED] = g_signal_new(NM_DEVICE_WIFI_P2P_DEVICE_CREATED,
+                                               G_OBJECT_CLASS_TYPE(object_class),
+                                               G_SIGNAL_RUN_LAST,
+                                               0,
+                                               NULL,
+                                               NULL,
+                                               g_cclosure_marshal_VOID__OBJECT,
+                                               G_TYPE_NONE,
+                                               1,
+                                               NM_TYPE_DEVICE);
+}
diff --git a/src/core/devices/wifi/nm-device-wifi.h b/src/core/devices/wifi/nm-device-wifi.h
new file mode 100644
index 00000000..d9e9038c
--- /dev/null
+++ b/src/core/devices/wifi/nm-device-wifi.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2005 - 2016 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_WIFI_H__
+#define __NETWORKMANAGER_DEVICE_WIFI_H__
+
+#include "devices/nm-device.h"
+
+#define NM_TYPE_DEVICE_WIFI (nm_device_wifi_get_type())
+#define NM_DEVICE_WIFI(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifi))
+#define NM_DEVICE_WIFI_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass))
+#define NM_IS_DEVICE_WIFI(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_WIFI))
+#define NM_IS_DEVICE_WIFI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_WIFI))
+#define NM_DEVICE_WIFI_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_WIFI, NMDeviceWifiClass))
+
+#define NM_DEVICE_WIFI_MODE                "mode"
+#define NM_DEVICE_WIFI_BITRATE             "bitrate"
+#define NM_DEVICE_WIFI_ACCESS_POINTS       "access-points"
+#define NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT "active-access-point"
+#define NM_DEVICE_WIFI_CAPABILITIES        "wireless-capabilities"
+#define NM_DEVICE_WIFI_SCANNING            "scanning"
+#define NM_DEVICE_WIFI_LAST_SCAN           "last-scan"
+
+#define NM_DEVICE_WIFI_P2P_DEVICE_CREATED "p2p-device-created"
+
+typedef struct _NMDeviceWifi      NMDeviceWifi;
+typedef struct _NMDeviceWifiClass NMDeviceWifiClass;
+
+GType nm_device_wifi_get_type(void);
+
+NMDevice *nm_device_wifi_new(const char *iface, NMDeviceWifiCapabilities capabilities);
+
+const CList *_nm_device_wifi_get_aps(NMDeviceWifi *self);
+
+void _nm_device_wifi_request_scan(NMDeviceWifi *         self,
+                                  GVariant *             options,
+                                  GDBusMethodInvocation *invocation);
+
+GPtrArray *nmtst_ssids_options_to_ptrarray(GVariant *value, GError **error);
+
+gboolean nm_device_wifi_get_scanning(NMDeviceWifi *self);
+
+void nm_device_wifi_scanning_prohibited_track(NMDeviceWifi *self,
+                                              gpointer      tag,
+                                              gboolean      temporarily_prohibited);
+
+#endif /* __NETWORKMANAGER_DEVICE_WIFI_H__ */
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
new file mode 100644
index 00000000..b4b019d3
--- /dev/null
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -0,0 +1,1359 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Intel Corporation
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-iwd-manager.h"
+
+#include <net/if.h>
+
+#include "nm-core-internal.h"
+#include "nm-manager.h"
+#include "nm-device-iwd.h"
+#include "nm-wifi-utils.h"
+#include "nm-glib-aux/nm-random-utils.h"
+#include "settings/nm-settings.h"
+#include "nm-std-aux/nm-dbus-compat.h"
+
+/*****************************************************************************/
+
+typedef struct {
+    const char *         name;
+    NMIwdNetworkSecurity security;
+    char                 buf[0];
+} KnownNetworkId;
+
+typedef struct {
+    GDBusProxy *          known_network;
+    NMSettingsConnection *mirror_connection;
+} KnownNetworkData;
+
+typedef struct {
+    NMManager *         manager;
+    NMSettings *        settings;
+    GCancellable *      cancellable;
+    gboolean            running;
+    GDBusObjectManager *object_manager;
+    guint               agent_id;
+    char *              agent_path;
+    GHashTable *        known_networks;
+    NMDeviceIwd *       last_agent_call_device;
+} NMIwdManagerPrivate;
+
+struct _NMIwdManager {
+    GObject             parent;
+    NMIwdManagerPrivate _priv;
+};
+
+struct _NMIwdManagerClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMIwdManager, nm_iwd_manager, G_TYPE_OBJECT)
+
+#define NM_IWD_MANAGER_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMIwdManager, NM_IS_IWD_MANAGER)
+
+/*****************************************************************************/
+
+#define _NMLOG_PREFIX_NAME "iwd-manager"
+#define _NMLOG_DOMAIN      LOGD_WIFI
+
+#define _NMLOG(level, ...)                                                 \
+    G_STMT_START                                                           \
+    {                                                                      \
+        if (nm_logging_enabled(level, _NMLOG_DOMAIN)) {                    \
+            char __prefix[32];                                             \
+                                                                           \
+            if (self)                                                      \
+                g_snprintf(__prefix,                                       \
+                           sizeof(__prefix),                               \
+                           "%s[%p]",                                       \
+                           ""_NMLOG_PREFIX_NAME                            \
+                           "",                                             \
+                           (self));                                        \
+            else                                                           \
+                g_strlcpy(__prefix, _NMLOG_PREFIX_NAME, sizeof(__prefix)); \
+            _nm_log((level),                                               \
+                    (_NMLOG_DOMAIN),                                       \
+                    0,                                                     \
+                    NULL,                                                  \
+                    NULL,                                                  \
+                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),             \
+                    __prefix _NM_UTILS_MACRO_REST(__VA_ARGS__));           \
+        }                                                                  \
+    }                                                                      \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void mirror_connection_take_and_delete(NMSettingsConnection *sett_conn,
+                                              KnownNetworkData *    data);
+
+/*****************************************************************************/
+
+static const char *
+get_variant_string_or_null(GVariant *v)
+{
+    if (!v)
+        return NULL;
+
+    if (!g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)
+        && !g_variant_is_of_type(v, G_VARIANT_TYPE_OBJECT_PATH))
+        return NULL;
+
+    return g_variant_get_string(v, NULL);
+}
+
+static const char *
+get_property_string_or_null(GDBusProxy *proxy, const char *property)
+{
+    gs_unref_variant GVariant *value = NULL;
+
+    if (!proxy || !property)
+        return NULL;
+
+    value = g_dbus_proxy_get_cached_property(proxy, property);
+
+    return get_variant_string_or_null(value);
+}
+
+static gboolean
+get_property_bool(GDBusProxy *proxy, const char *property, gboolean default_val)
+{
+    gs_unref_variant GVariant *value = NULL;
+
+    if (!proxy || !property)
+        return default_val;
+
+    value = g_dbus_proxy_get_cached_property(proxy, property);
+    if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE_BOOLEAN))
+        return default_val;
+
+    return g_variant_get_boolean(value);
+}
+
+static NMDeviceIwd *
+get_device_from_network(NMIwdManager *self, GDBusProxy *network)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    const char *         ifname;
+    const char *         device_path;
+    NMDevice *           device;
+    gs_unref_object GDBusInterface *device_obj = NULL;
+
+    /* Try not to rely on the path of the Device being a prefix of the
+     * Network's object path.
+     */
+
+    device_path = get_property_string_or_null(network, "Device");
+    if (!device_path) {
+        _LOGD("Device not cached for network at %s", g_dbus_proxy_get_object_path(network));
+        return NULL;
+    }
+
+    device_obj = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                     device_path,
+                                                     NM_IWD_DEVICE_INTERFACE);
+
+    ifname = get_property_string_or_null(G_DBUS_PROXY(device_obj), "Name");
+    if (!ifname) {
+        _LOGD("Name not cached for device at %s", device_path);
+        return NULL;
+    }
+
+    device = nm_manager_get_device(priv->manager, ifname, NM_DEVICE_TYPE_WIFI);
+    if (!device || !NM_IS_DEVICE_IWD(device)) {
+        _LOGD("NM device %s is not an IWD-managed device", ifname);
+        return NULL;
+    }
+
+    return NM_DEVICE_IWD(device);
+}
+
+static void
+agent_dbus_method_cb(GDBusConnection *      connection,
+                     const char *           sender,
+                     const char *           object_path,
+                     const char *           interface_name,
+                     const char *           method_name,
+                     GVariant *             parameters,
+                     GDBusMethodInvocation *invocation,
+                     gpointer               user_data)
+{
+    NMIwdManager *       self = user_data;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    const char *         network_path;
+    NMDeviceIwd *        device;
+    gs_free char *       name_owner         = NULL;
+    gs_unref_object GDBusInterface *network = NULL;
+
+    /* Be paranoid and check the sender address */
+    name_owner = g_dbus_object_manager_client_get_name_owner(
+        G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager));
+    if (!nm_streq0(name_owner, sender))
+        goto return_error;
+
+    if (!strcmp(method_name, "Cancel")) {
+        const char *reason = NULL;
+
+        g_variant_get(parameters, "(&s)", &reason);
+        _LOGD("agent-request: Cancel reason: %s", reason);
+
+        if (!priv->last_agent_call_device)
+            goto return_error;
+
+        if (nm_device_iwd_agent_query(priv->last_agent_call_device, NULL)) {
+            priv->last_agent_call_device = NULL;
+            g_dbus_method_invocation_return_value(invocation, NULL);
+            return;
+        }
+
+        priv->last_agent_call_device = NULL;
+        goto return_error;
+    }
+
+    if (!strcmp(method_name, "RequestUserPassword"))
+        g_variant_get(parameters, "(&os)", &network_path, NULL);
+    else
+        g_variant_get(parameters, "(&o)", &network_path);
+
+    network = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                  network_path,
+                                                  NM_IWD_NETWORK_INTERFACE);
+    if (!network) {
+        _LOGE("agent-request: unable to find the network object");
+        goto return_error;
+    }
+
+    device = get_device_from_network(self, G_DBUS_PROXY(network));
+    if (!device) {
+        _LOGD("agent-request: device not found in IWD Agent request");
+        goto return_error;
+    }
+
+    if (nm_device_iwd_agent_query(device, invocation)) {
+        priv->last_agent_call_device = device;
+        return;
+    }
+
+    _LOGD("agent-request: device %s did not handle the IWD Agent request",
+          nm_device_get_iface(NM_DEVICE(device)));
+
+return_error:
+    /* IWD doesn't look at the specific error */
+    g_dbus_method_invocation_return_error_literal(invocation,
+                                                  NM_DEVICE_ERROR,
+                                                  NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                  "Secrets not available for this connection");
+}
+
+static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+    "net.connman.iwd.Agent",
+    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+        NM_DEFINE_GDBUS_METHOD_INFO(
+            "RequestPassphrase",
+            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ),
+            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("passphrase", "s"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO(
+            "RequestPrivateKeyPassphrase",
+            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ),
+            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("passphrase", "s"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO(
+            "RequestUserNameAndPassword",
+            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"), ),
+            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("user", "s"),
+                                                  NM_DEFINE_GDBUS_ARG_INFO("password", "s"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO(
+            "RequestUserPassword",
+            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("network", "o"),
+                                                 NM_DEFINE_GDBUS_ARG_INFO("user", "s"), ),
+            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("password", "s"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO("Cancel",
+                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                        NM_DEFINE_GDBUS_ARG_INFO("reason", "s"), ), ), ), );
+
+static guint
+iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_path, GError **error)
+{
+    static const GDBusInterfaceVTable vtable = {
+        .method_call = agent_dbus_method_cb,
+    };
+    char         path[50];
+    unsigned int rnd;
+    guint        id;
+
+    nm_utils_random_bytes(&rnd, sizeof(rnd));
+
+    nm_sprintf_buf(path, "/agent/%u", rnd);
+
+    id =
+        g_dbus_connection_register_object(connection,
+                                          path,
+                                          NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
+                                          &vtable,
+                                          user_data,
+                                          NULL,
+                                          error);
+
+    if (id)
+        *agent_path = g_strdup(path);
+    return id;
+}
+
+static void
+register_agent(NMIwdManager *self)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusInterface *     agent_manager;
+
+    agent_manager = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                        "/net/connman/iwd", /* IWD 1.0+ */
+                                                        NM_IWD_AGENT_MANAGER_INTERFACE);
+    if (!agent_manager) {
+        _LOGE("unable to register the IWD Agent: PSK/8021x Wi-Fi networks may not work");
+        return;
+    }
+
+    /* Register our agent */
+    g_dbus_proxy_call(G_DBUS_PROXY(agent_manager),
+                      "RegisterAgent",
+                      g_variant_new("(o)", priv->agent_path),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      -1,
+                      NULL,
+                      NULL,
+                      NULL);
+
+    g_object_unref(agent_manager);
+}
+
+/*****************************************************************************/
+
+static KnownNetworkId *
+known_network_id_new(const char *name, NMIwdNetworkSecurity security)
+{
+    KnownNetworkId *id;
+    gsize           strsize = strlen(name) + 1;
+
+    id           = g_malloc(sizeof(KnownNetworkId) + strsize);
+    id->name     = id->buf;
+    id->security = security;
+    memcpy(id->buf, name, strsize);
+
+    return id;
+}
+
+static guint
+known_network_id_hash(KnownNetworkId *id)
+{
+    NMHashState h;
+
+    nm_hash_init(&h, 1947951703u);
+    nm_hash_update_val(&h, id->security);
+    nm_hash_update_str(&h, id->name);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+known_network_id_equal(KnownNetworkId *a, KnownNetworkId *b)
+{
+    return a->security == b->security && nm_streq(a->name, b->name);
+}
+
+static void
+known_network_data_free(KnownNetworkData *network)
+{
+    if (!network)
+        return;
+
+    g_object_unref(network->known_network);
+    mirror_connection_take_and_delete(network->mirror_connection, network);
+    g_slice_free(KnownNetworkData, network);
+}
+
+/*****************************************************************************/
+
+static void
+set_device_dbus_object(NMIwdManager *self, GDBusProxy *proxy, GDBusObject *object)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    const char *         ifname;
+    int                  ifindex;
+    NMDevice *           device;
+    int                  errsv;
+
+    ifname = get_property_string_or_null(proxy, "Name");
+    if (!ifname) {
+        _LOGE("Name not cached for Device at %s", g_dbus_proxy_get_object_path(proxy));
+        return;
+    }
+
+    ifindex = if_nametoindex(ifname);
+
+    if (!ifindex) {
+        errsv = errno;
+        _LOGE("if_nametoindex failed for Name %s for Device at %s: %i",
+              ifname,
+              g_dbus_proxy_get_object_path(proxy),
+              errsv);
+        return;
+    }
+
+    device = nm_manager_get_device_by_ifindex(priv->manager, ifindex);
+    if (!NM_IS_DEVICE_IWD(device)) {
+        _LOGE("IWD device named %s is not a Wifi device", ifname);
+        return;
+    }
+
+    nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), object);
+}
+
+static void
+known_network_update_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    gs_unref_variant GVariant *variant = NULL;
+    gs_free_error GError *error        = NULL;
+
+    variant = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
+    if (!variant) {
+        nm_log_warn(LOGD_WIFI,
+                    "Updating %s on IWD known network %s failed: %s",
+                    (const char *) user_data,
+                    g_dbus_proxy_get_object_path(G_DBUS_PROXY(source)),
+                    error->message);
+    }
+}
+
+static void
+sett_conn_changed(NMSettingsConnection *sett_conn, guint update_reason, KnownNetworkData *data)
+{
+    NMSettingsConnectionIntFlags flags;
+    NMConnection *               conn   = nm_settings_connection_get_connection(sett_conn);
+    NMSettingConnection *        s_conn = nm_connection_get_setting_connection(conn);
+    gboolean                     nm_autoconnectable = nm_setting_connection_get_autoconnect(s_conn);
+    gboolean iwd_autoconnectable = get_property_bool(data->known_network, "AutoConnect", TRUE);
+
+    nm_assert(sett_conn == data->mirror_connection);
+
+    if (iwd_autoconnectable == nm_autoconnectable)
+        return;
+
+    /* If this is a generated connection it may be ourselves updating it */
+    flags = nm_settings_connection_get_flags(data->mirror_connection);
+    if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
+        return;
+
+    nm_log_dbg(LOGD_WIFI,
+               "Updating AutoConnect on known network at %s based on connection %s",
+               g_dbus_proxy_get_object_path(data->known_network),
+               nm_settings_connection_get_id(data->mirror_connection));
+    g_dbus_proxy_call(data->known_network,
+                      DBUS_INTERFACE_PROPERTIES ".Set",
+                      g_variant_new("(ssv)",
+                                    NM_IWD_KNOWN_NETWORK_INTERFACE,
+                                    "AutoConnect",
+                                    g_variant_new_boolean(nm_autoconnectable)),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      -1,
+                      NULL,
+                      known_network_update_cb,
+                      "AutoConnect");
+}
+
+/* Look up an existing NMSettingsConnection for a network that has been
+ * preprovisioned with an IWD config file or has been connected to before,
+ * or create a new in-memory NMSettingsConnection object.  This will let
+ * users control the few supported properties (mainly make it
+ * IWD-autoconnectable or not), remove/forget the network, or, for a
+ * WPA2-Enterprise type network it will inform the NM autoconnect mechanism
+ * and the clients that this networks needs no additional EAP configuration
+ * from the user.
+ */
+static NMSettingsConnection *
+mirror_connection(NMIwdManager *        self,
+                  const KnownNetworkId *id,
+                  gboolean              create_new,
+                  GDBusProxy *          known_network)
+{
+    NMIwdManagerPrivate *        priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    NMSettingsConnection *const *iter;
+    gs_unref_object NMConnection *connection          = NULL;
+    NMSettingsConnection *        settings_connection = NULL;
+    char                          uuid[37];
+    NMSetting *                   setting;
+    gs_free_error GError *error            = NULL;
+    gs_unref_bytes GBytes *new_ssid        = NULL;
+    gsize                  ssid_len        = strlen(id->name);
+    gboolean               autoconnectable = TRUE;
+    gboolean               hidden          = FALSE;
+    gboolean               exact_match     = TRUE;
+    const char *           key_mgmt        = NULL;
+
+    if (known_network) {
+        autoconnectable = get_property_bool(known_network, "AutoConnect", TRUE);
+        hidden          = get_property_bool(known_network, "Hidden", FALSE);
+    }
+
+    for (iter = nm_settings_get_connections(priv->settings, NULL); *iter; iter++) {
+        NMSettingsConnection *sett_conn = *iter;
+        NMConnection *        conn      = nm_settings_connection_get_connection(sett_conn);
+        NMIwdNetworkSecurity  security;
+        NMSettingWireless *   s_wifi;
+        const guint8 *        ssid_bytes;
+        gsize                 ssid_len2;
+
+        if (!nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &security))
+            continue;
+
+        if (security != id->security)
+            continue;
+
+        s_wifi = nm_connection_get_setting_wireless(conn);
+        if (!s_wifi)
+            continue;
+
+        /* The SSID must be UTF-8 if it matches since id->name is known to be
+         * valid UTF-8, so just memcmp them.
+         */
+        ssid_bytes = g_bytes_get_data(nm_setting_wireless_get_ssid(s_wifi), &ssid_len2);
+        if (!ssid_bytes || ssid_len2 != ssid_len || memcmp(ssid_bytes, id->name, ssid_len))
+            continue;
+
+        exact_match = TRUE;
+
+        if (known_network) {
+            NMSettingConnection *s_conn = nm_connection_get_setting_connection(conn);
+
+            if (nm_setting_connection_get_autoconnect(s_conn) != autoconnectable
+                || nm_setting_wireless_get_hidden(s_wifi) != hidden)
+                exact_match = FALSE;
+        }
+
+        switch (id->security) {
+        case NM_IWD_NETWORK_SECURITY_WEP:
+        case NM_IWD_NETWORK_SECURITY_OPEN:
+        case NM_IWD_NETWORK_SECURITY_PSK:
+            break;
+        case NM_IWD_NETWORK_SECURITY_8021X:
+        {
+            NMSetting8021x *s_8021x  = nm_connection_get_setting_802_1x(conn);
+            gboolean        external = FALSE;
+            guint           i;
+
+            for (i = 0; i < nm_setting_802_1x_get_num_eap_methods(s_8021x); i++) {
+                if (nm_streq(nm_setting_802_1x_get_eap_method(s_8021x, i), "external")) {
+                    external = TRUE;
+                    break;
+                }
+            }
+
+            /* Prefer returning connections with EAP method "external" */
+            if (!external)
+                exact_match = FALSE;
+        }
+        }
+
+        if (!settings_connection || exact_match)
+            settings_connection = sett_conn;
+
+        if (exact_match)
+            break;
+    }
+
+    if (settings_connection && known_network && !exact_match) {
+        NMSettingsConnectionIntFlags flags = nm_settings_connection_get_flags(settings_connection);
+
+        /* If we found a connection and it's generated (likely by ourselves)
+         * it may have been created on a request by
+         * nm_iwd_manager_get_ap_mirror_connection() when no Known Network
+         * was available so we didn't have access to its properties other
+         * than Name and Security.  Copy their values to the generated
+         * NMConnection.
+         * TODO: avoid notify signals triggering our own watch.
+         *
+         * If on the other hand this is a user-created NMConnection we
+         * should try to copy the properties from it to IWD's Known Network
+         * using the Properties DBus interface in case the user created an
+         * NM connection before IWD appeared on the bus, or before IWD
+         * created its Known Network object.
+         */
+        if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+            NMConnection *tmp_conn = nm_settings_connection_get_connection(settings_connection);
+            NMSettingConnection *s_conn = nm_connection_get_setting_connection(tmp_conn);
+            NMSettingWireless *  s_wifi = nm_connection_get_setting_wireless(tmp_conn);
+
+            g_object_set(G_OBJECT(s_conn),
+                         NM_SETTING_CONNECTION_AUTOCONNECT,
+                         autoconnectable,
+                         NULL);
+            g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_HIDDEN, hidden, NULL);
+        } else {
+            KnownNetworkData data = {known_network, settings_connection};
+            sett_conn_changed(settings_connection, 0, &data);
+        }
+    }
+
+    if (settings_connection && known_network) {
+        /* Reset NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL now that the
+         * connection is going to be referenced by a known network, we don't
+         * want it to be deleted when activation fails anymore.
+         */
+        nm_settings_connection_set_flags_full(settings_connection,
+                                              NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL,
+                                              0);
+    }
+
+    /* If we already have an NMSettingsConnection matching this
+     * KnownNetwork, whether it's saved or an in-memory connection
+     * potentially created by ourselves then we have nothing left to
+     * do here.
+     */
+    if (settings_connection || !create_new)
+        return settings_connection;
+
+    connection = nm_simple_connection_new();
+
+    setting = g_object_new(NM_TYPE_SETTING_CONNECTION,
+                           NM_SETTING_CONNECTION_TYPE,
+                           NM_SETTING_WIRELESS_SETTING_NAME,
+                           NM_SETTING_CONNECTION_ID,
+                           id->name,
+                           NM_SETTING_CONNECTION_UUID,
+                           nm_utils_uuid_generate_buf(uuid),
+                           NM_SETTING_CONNECTION_AUTOCONNECT,
+                           autoconnectable,
+                           NULL);
+    nm_connection_add_setting(connection, setting);
+
+    new_ssid = g_bytes_new(id->name, ssid_len);
+    setting  = g_object_new(NM_TYPE_SETTING_WIRELESS,
+                           NM_SETTING_WIRELESS_SSID,
+                           new_ssid,
+                           NM_SETTING_WIRELESS_MODE,
+                           NM_SETTING_WIRELESS_MODE_INFRA,
+                           NM_SETTING_WIRELESS_HIDDEN,
+                           hidden,
+                           NULL);
+    nm_connection_add_setting(connection, setting);
+
+    switch (id->security) {
+    case NM_IWD_NETWORK_SECURITY_WEP:
+        key_mgmt = "none";
+        break;
+    case NM_IWD_NETWORK_SECURITY_OPEN:
+        key_mgmt = NULL;
+        break;
+    case NM_IWD_NETWORK_SECURITY_PSK:
+        key_mgmt = "wpa-psk";
+        break;
+    case NM_IWD_NETWORK_SECURITY_8021X:
+        key_mgmt = "wpa-eap";
+        break;
+    }
+
+    if (key_mgmt) {
+        setting = g_object_new(NM_TYPE_SETTING_WIRELESS_SECURITY,
+                               NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                               "open",
+                               NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                               key_mgmt,
+                               NULL);
+        nm_connection_add_setting(connection, setting);
+    }
+
+    if (id->security == NM_IWD_NETWORK_SECURITY_8021X) {
+        /* "password" and "private-key-password" may be requested by the IWD agent
+         * from NM and IWD will implement a specific secret cache policy so by
+         * default respect that policy and don't save copies of those secrets in
+         * NM settings.  The saved values can not be used anyway because of our
+         * use of NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW.
+         */
+        setting = g_object_new(NM_TYPE_SETTING_802_1X,
+                               NM_SETTING_802_1X_PASSWORD_FLAGS,
+                               NM_SETTING_SECRET_FLAG_NOT_SAVED,
+                               NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD_FLAGS,
+                               NM_SETTING_SECRET_FLAG_NOT_SAVED,
+                               NULL);
+        nm_setting_802_1x_add_eap_method(NM_SETTING_802_1X(setting), "external");
+        nm_connection_add_setting(connection, setting);
+    }
+
+    if (!nm_connection_normalize(connection, NULL, NULL, NULL))
+        return NULL;
+
+    if (!nm_settings_add_connection(
+            priv->settings,
+            connection,
+            NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+            NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+            NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED
+                | (known_network ? 0 : NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL),
+            &settings_connection,
+            &error)) {
+        _LOGW("failed to add a mirror NMConnection for IWD's Known Network '%s': %s",
+              id->name,
+              error->message);
+        return NULL;
+    }
+
+    return settings_connection;
+}
+
+static void
+mirror_connection_take_and_delete(NMSettingsConnection *sett_conn, KnownNetworkData *data)
+{
+    NMSettingsConnectionIntFlags flags;
+
+    if (!sett_conn)
+        return;
+
+    flags = nm_settings_connection_get_flags(sett_conn);
+
+    /* If connection has not been saved since we created it
+     * in interface_added it too can be removed now. */
+    if (NM_FLAGS_HAS(flags, NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
+        nm_settings_connection_delete(sett_conn, FALSE);
+
+    g_signal_handlers_disconnect_by_data(sett_conn, data);
+    g_object_unref(sett_conn);
+}
+
+static void
+interface_added(GDBusObjectManager *object_manager,
+                GDBusObject *       object,
+                GDBusInterface *    interface,
+                gpointer            user_data)
+{
+    NMIwdManager *       self = user_data;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusProxy *         proxy;
+    const char *         iface_name;
+
+    if (!priv->running)
+        return;
+
+    g_return_if_fail(G_IS_DBUS_PROXY(interface));
+
+    proxy      = G_DBUS_PROXY(interface);
+    iface_name = g_dbus_proxy_get_interface_name(proxy);
+
+    if (nm_streq(iface_name, NM_IWD_DEVICE_INTERFACE)) {
+        set_device_dbus_object(self, proxy, object);
+        return;
+    }
+
+    if (nm_streq(iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) {
+        KnownNetworkId *      id;
+        KnownNetworkId *      orig_id;
+        KnownNetworkData *    data;
+        NMIwdNetworkSecurity  security;
+        const char *          type_str, *name;
+        NMSettingsConnection *sett_conn = NULL;
+
+        type_str = get_property_string_or_null(proxy, "Type");
+        name     = get_property_string_or_null(proxy, "Name");
+        if (!type_str || !name)
+            return;
+
+        if (nm_streq(type_str, "open"))
+            security = NM_IWD_NETWORK_SECURITY_OPEN;
+        else if (nm_streq(type_str, "psk"))
+            security = NM_IWD_NETWORK_SECURITY_PSK;
+        else if (nm_streq(type_str, "8021x"))
+            security = NM_IWD_NETWORK_SECURITY_8021X;
+        else
+            return;
+
+        id = known_network_id_new(name, security);
+
+        if (g_hash_table_lookup_extended(priv->known_networks,
+                                         id,
+                                         (void **) &orig_id,
+                                         (void **) &data)) {
+            _LOGW("DBus error: KnownNetwork already exists ('%s', %s)", name, type_str);
+            nm_g_object_ref_set(&data->known_network, proxy);
+            g_free(id);
+            id = orig_id;
+        } else {
+            data                = g_slice_new0(KnownNetworkData);
+            data->known_network = g_object_ref(proxy);
+            g_hash_table_insert(priv->known_networks, id, data);
+        }
+
+        sett_conn = mirror_connection(self, id, TRUE, proxy);
+
+        if (sett_conn && sett_conn != data->mirror_connection) {
+            NMSettingsConnection *sett_conn_old = data->mirror_connection;
+
+            data->mirror_connection = nm_g_object_ref(sett_conn);
+            mirror_connection_take_and_delete(sett_conn_old, data);
+
+            g_signal_connect(sett_conn,
+                             NM_SETTINGS_CONNECTION_UPDATED_INTERNAL,
+                             G_CALLBACK(sett_conn_changed),
+                             data);
+        }
+
+        return;
+    }
+
+    if (nm_streq(iface_name, NM_IWD_NETWORK_INTERFACE)) {
+        NMDeviceIwd *device = get_device_from_network(self, proxy);
+
+        if (device)
+            nm_device_iwd_network_add_remove(device, proxy, TRUE);
+
+        return;
+    }
+}
+
+static void
+interface_removed(GDBusObjectManager *object_manager,
+                  GDBusObject *       object,
+                  GDBusInterface *    interface,
+                  gpointer            user_data)
+{
+    NMIwdManager *       self = user_data;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusProxy *         proxy;
+    const char *         iface_name;
+
+    g_return_if_fail(G_IS_DBUS_PROXY(interface));
+
+    proxy      = G_DBUS_PROXY(interface);
+    iface_name = g_dbus_proxy_get_interface_name(proxy);
+
+    if (nm_streq(iface_name, NM_IWD_DEVICE_INTERFACE)) {
+        set_device_dbus_object(self, proxy, NULL);
+        return;
+    }
+
+    if (nm_streq(iface_name, NM_IWD_KNOWN_NETWORK_INTERFACE)) {
+        KnownNetworkId id;
+        const char *   type_str;
+
+        type_str = get_property_string_or_null(proxy, "Type");
+        id.name  = get_property_string_or_null(proxy, "Name");
+        if (!type_str || !id.name)
+            return;
+
+        if (nm_streq(type_str, "open"))
+            id.security = NM_IWD_NETWORK_SECURITY_OPEN;
+        else if (nm_streq(type_str, "psk"))
+            id.security = NM_IWD_NETWORK_SECURITY_PSK;
+        else if (nm_streq(type_str, "8021x"))
+            id.security = NM_IWD_NETWORK_SECURITY_8021X;
+        else
+            return;
+
+        g_hash_table_remove(priv->known_networks, &id);
+        return;
+    }
+
+    if (nm_streq(iface_name, NM_IWD_NETWORK_INTERFACE)) {
+        NMDeviceIwd *device = get_device_from_network(self, proxy);
+
+        if (device)
+            nm_device_iwd_network_add_remove(device, proxy, FALSE);
+
+        return;
+    }
+}
+
+static void
+object_added(GDBusObjectManager *object_manager, GDBusObject *object, gpointer user_data)
+{
+    GList *interfaces, *iter;
+
+    interfaces = g_dbus_object_get_interfaces(object);
+
+    for (iter = interfaces; iter; iter = iter->next) {
+        GDBusInterface *interface = G_DBUS_INTERFACE(iter->data);
+
+        interface_added(NULL, object, interface, user_data);
+    }
+
+    g_list_free_full(interfaces, g_object_unref);
+}
+
+static void
+object_removed(GDBusObjectManager *object_manager, GDBusObject *object, gpointer user_data)
+{
+    GList *interfaces, *iter;
+
+    interfaces = g_dbus_object_get_interfaces(object);
+
+    for (iter = interfaces; iter; iter = iter->next) {
+        GDBusInterface *interface = G_DBUS_INTERFACE(iter->data);
+
+        interface_removed(NULL, object, interface, user_data);
+    }
+
+    g_list_free_full(interfaces, g_object_unref);
+}
+
+static void
+connection_removed(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer user_data)
+{
+    NMIwdManager *        self = user_data;
+    NMIwdManagerPrivate * priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    NMConnection *        conn = nm_settings_connection_get_connection(sett_conn);
+    NMSettingWireless *   s_wireless;
+    KnownNetworkData *    data;
+    KnownNetworkId        id;
+    char                  ssid_buf[33];
+    const guint8 *        ssid_bytes;
+    gsize                 ssid_len;
+    NMSettingsConnection *new_mirror_conn;
+
+    if (!nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &id.security))
+        return;
+
+    s_wireless = nm_connection_get_setting_wireless(conn);
+    if (!s_wireless)
+        return;
+
+    ssid_bytes = g_bytes_get_data(nm_setting_wireless_get_ssid(s_wireless), &ssid_len);
+    if (!ssid_bytes || ssid_len > 32 || memchr(ssid_bytes, 0, ssid_len))
+        return;
+
+    memcpy(ssid_buf, ssid_bytes, ssid_len);
+    ssid_buf[ssid_len] = '\0';
+    id.name            = ssid_buf;
+    data               = g_hash_table_lookup(priv->known_networks, &id);
+    if (!data)
+        return;
+
+    if (data->mirror_connection != sett_conn)
+        return;
+
+    g_clear_object(&data->mirror_connection);
+
+    /* Don't call Forget on the Known Network until there's no longer *any*
+     * matching NMSettingsConnection (debatable)
+     */
+    new_mirror_conn = mirror_connection(self, &id, FALSE, NULL);
+    if (new_mirror_conn) {
+        data->mirror_connection = g_object_ref(new_mirror_conn);
+        return;
+    }
+
+    if (!priv->running)
+        return;
+
+    g_dbus_proxy_call(data->known_network,
+                      "Forget",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      -1,
+                      NULL,
+                      NULL,
+                      NULL);
+}
+
+static gboolean
+_om_has_name_owner(GDBusObjectManager *object_manager)
+{
+    gs_free char *name_owner = NULL;
+
+    nm_assert(G_IS_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
+
+    name_owner =
+        g_dbus_object_manager_client_get_name_owner(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
+    return !!name_owner;
+}
+
+static void
+release_object_manager(NMIwdManager *self)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+
+    if (!priv->object_manager)
+        return;
+
+    g_signal_handlers_disconnect_by_data(priv->object_manager, self);
+
+    if (priv->agent_id) {
+        GDBusConnection *         agent_connection;
+        GDBusObjectManagerClient *omc = G_DBUS_OBJECT_MANAGER_CLIENT(priv->object_manager);
+
+        agent_connection = g_dbus_object_manager_client_get_connection(omc);
+
+        /* We're is called when we're shutting down (i.e. our DBus connection
+         * is being closed, and IWD will detect this) or IWD was stopped so
+         * in either case calling UnregisterAgent will not do anything.
+         */
+        g_dbus_connection_unregister_object(agent_connection, priv->agent_id);
+        priv->agent_id = 0;
+        nm_clear_g_free(&priv->agent_path);
+    }
+
+    g_clear_object(&priv->object_manager);
+}
+
+static void prepare_object_manager(NMIwdManager *self);
+
+static void
+name_owner_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    NMIwdManager *       self           = user_data;
+    NMIwdManagerPrivate *priv           = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusObjectManager * object_manager = G_DBUS_OBJECT_MANAGER(object);
+
+    nm_assert(object_manager == priv->object_manager);
+
+    if (_om_has_name_owner(object_manager)) {
+        release_object_manager(self);
+        prepare_object_manager(self);
+    } else {
+        const CList *tmp_lst;
+        NMDevice *   device;
+
+        if (!priv->running)
+            return;
+
+        priv->running = false;
+
+        nm_manager_for_each_device (priv->manager, device, tmp_lst) {
+            if (NM_IS_DEVICE_IWD(device)) {
+                nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), NULL);
+            }
+        }
+    }
+}
+
+static void
+device_added(NMManager *manager, NMDevice *device, gpointer user_data)
+{
+    NMIwdManager *       self = user_data;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GList *              objects, *iter;
+
+    if (!NM_IS_DEVICE_IWD(device))
+        return;
+
+    if (!priv->running)
+        return;
+
+    /* Here we handle a potential scenario where IWD's DBus objects for the
+     * new device popped up before the NMDevice.  The
+     * interface_added/object_added signals have been received already and
+     * the handlers couldn't do much because the NMDevice wasn't there yet
+     * so now we go over the Network and Device interfaces again.  In this
+     * exact order for "object path" property consistency -- see reasoning
+     * in object_compare_interfaces.
+     */
+    objects = g_dbus_object_manager_get_objects(priv->object_manager);
+
+    for (iter = objects; iter; iter = iter->next) {
+        GDBusObject *   object                    = G_DBUS_OBJECT(iter->data);
+        gs_unref_object GDBusInterface *interface = NULL;
+
+        interface = g_dbus_object_get_interface(object, NM_IWD_NETWORK_INTERFACE);
+        if (!interface)
+            continue;
+
+        if (NM_DEVICE_IWD(device) == get_device_from_network(self, (GDBusProxy *) interface))
+            nm_device_iwd_network_add_remove(NM_DEVICE_IWD(device), (GDBusProxy *) interface, TRUE);
+    }
+
+    for (iter = objects; iter; iter = iter->next) {
+        GDBusObject *   object                    = G_DBUS_OBJECT(iter->data);
+        gs_unref_object GDBusInterface *interface = NULL;
+        const char *                    obj_ifname;
+
+        interface  = g_dbus_object_get_interface(object, NM_IWD_DEVICE_INTERFACE);
+        obj_ifname = get_property_string_or_null((GDBusProxy *) interface, "Name");
+
+        if (!obj_ifname || strcmp(nm_device_get_iface(device), obj_ifname))
+            continue;
+
+        nm_device_iwd_set_dbus_object(NM_DEVICE_IWD(device), object);
+        break;
+    }
+
+    g_list_free_full(objects, g_object_unref);
+}
+
+static void
+device_removed(NMManager *manager, NMDevice *device, gpointer user_data)
+{
+    NMIwdManager *       self = user_data;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+
+    if (!NM_IS_DEVICE_IWD(device))
+        return;
+
+    if (priv->last_agent_call_device == NM_DEVICE_IWD(device))
+        priv->last_agent_call_device = NULL;
+}
+
+/* This is used to sort the list of objects returned by GetManagedObjects()
+ * based on the DBus interfaces available on these objects in such a way that
+ * the interface_added calls happen in the right order.  The order is defined
+ * by how some DBus interfaces point to interfaces on other objects using
+ * DBus properties of the type "object path" ("o" signature).  This creates
+ * "dependencies" between objects.
+ *
+ * When NM and IWD are running, the InterfacesAdded signals should come in
+ * an order that ensures consistency of those object paths.  For example
+ * when a Network interface is added with a KnownNetwork property, or that
+ * property is assigned a new value, the KnownNetwork object pointed to by
+ * it will have been added in an earlier InterfacesAdded signal.  Similarly
+ * Station.ConnectedNetwork and Station.GetOrdereNetworks() only point to
+ * existing Network objects.  (There may be circular dependencies but during
+ * initialization we only need a subset of those properties that doesn't
+ * have this problem.)
+ *
+ * But GetManagedObjects doesn't guarantee this kind of consistency so we
+ * order the returned object list ourselves to simplify the job of
+ * interface_added().  Objects that don't have any interfaces listed in
+ * interface_order are moved to the end of the list.
+ */
+static int
+object_compare_interfaces(gconstpointer a, gconstpointer b)
+{
+    static const char *interface_order[] = {
+        NM_IWD_KNOWN_NETWORK_INTERFACE,
+        NM_IWD_NETWORK_INTERFACE,
+        NM_IWD_DEVICE_INTERFACE,
+    };
+    int   rank_a = G_N_ELEMENTS(interface_order);
+    int   rank_b = G_N_ELEMENTS(interface_order);
+    guint pos;
+
+    for (pos = 0; interface_order[pos]; pos++) {
+        GDBusInterface *iface_a;
+        GDBusInterface *iface_b;
+
+        if (rank_a == G_N_ELEMENTS(interface_order)
+            && (iface_a = g_dbus_object_get_interface(G_DBUS_OBJECT(a), interface_order[pos]))) {
+            rank_a = pos;
+            g_object_unref(iface_a);
+        }
+
+        if (rank_b == G_N_ELEMENTS(interface_order)
+            && (iface_b = g_dbus_object_get_interface(G_DBUS_OBJECT(b), interface_order[pos]))) {
+            rank_b = pos;
+            g_object_unref(iface_b);
+        }
+    }
+
+    return rank_a - rank_b;
+}
+
+static void
+got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
+{
+    NMIwdManager *       self  = user_data;
+    NMIwdManagerPrivate *priv  = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GError *             error = NULL;
+    GDBusObjectManager * object_manager;
+    GDBusConnection *    connection;
+
+    object_manager = g_dbus_object_manager_client_new_for_bus_finish(result, &error);
+    if (object_manager == NULL) {
+        _LOGE("failed to acquire IWD Object Manager: Wi-Fi will not be available (%s)",
+              error->message);
+        g_clear_error(&error);
+        return;
+    }
+
+    priv->object_manager = object_manager;
+
+    g_signal_connect(priv->object_manager,
+                     "notify::name-owner",
+                     G_CALLBACK(name_owner_changed),
+                     self);
+
+    nm_assert(G_IS_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
+
+    connection =
+        g_dbus_object_manager_client_get_connection(G_DBUS_OBJECT_MANAGER_CLIENT(object_manager));
+
+    priv->agent_id = iwd_agent_export(connection, self, &priv->agent_path, &error);
+    if (!priv->agent_id) {
+        _LOGE("failed to export the IWD Agent: PSK/8021x Wi-Fi networks may not work: %s",
+              error->message);
+        g_clear_error(&error);
+    }
+
+    if (_om_has_name_owner(object_manager)) {
+        GList *objects, *iter;
+
+        priv->running = true;
+
+        g_signal_connect(priv->object_manager,
+                         "interface-added",
+                         G_CALLBACK(interface_added),
+                         self);
+        g_signal_connect(priv->object_manager,
+                         "interface-removed",
+                         G_CALLBACK(interface_removed),
+                         self);
+        g_signal_connect(priv->object_manager, "object-added", G_CALLBACK(object_added), self);
+        g_signal_connect(priv->object_manager, "object-removed", G_CALLBACK(object_removed), self);
+
+        g_hash_table_remove_all(priv->known_networks);
+
+        objects = g_dbus_object_manager_get_objects(object_manager);
+        objects = g_list_sort(objects, object_compare_interfaces);
+        for (iter = objects; iter; iter = iter->next)
+            object_added(NULL, G_DBUS_OBJECT(iter->data), self);
+
+        g_list_free_full(objects, g_object_unref);
+
+        if (priv->agent_id)
+            register_agent(self);
+    }
+}
+
+static void
+prepare_object_manager(NMIwdManager *self)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+
+    g_dbus_object_manager_client_new_for_bus(NM_IWD_BUS_TYPE,
+                                             G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
+                                             NM_IWD_SERVICE,
+                                             "/",
+                                             NULL,
+                                             NULL,
+                                             NULL,
+                                             priv->cancellable,
+                                             got_object_manager,
+                                             self);
+}
+
+gboolean
+nm_iwd_manager_is_known_network(NMIwdManager *self, const char *name, NMIwdNetworkSecurity security)
+{
+    NMIwdManagerPrivate *priv  = NM_IWD_MANAGER_GET_PRIVATE(self);
+    KnownNetworkId       kn_id = {name, security};
+
+    return g_hash_table_contains(priv->known_networks, &kn_id);
+}
+
+NMSettingsConnection *
+nm_iwd_manager_get_ap_mirror_connection(NMIwdManager *self, NMWifiAP *ap)
+{
+    NMIwdManagerPrivate *  priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    KnownNetworkData *     data;
+    char                   name_buf[33];
+    KnownNetworkId         kn_id = {name_buf, NM_IWD_NETWORK_SECURITY_OPEN};
+    const guint8 *         ssid_bytes;
+    gsize                  ssid_len;
+    NM80211ApFlags         flags     = nm_wifi_ap_get_flags(ap);
+    NM80211ApSecurityFlags sec_flags = nm_wifi_ap_get_wpa_flags(ap) | nm_wifi_ap_get_rsn_flags(ap);
+
+    ssid_bytes = g_bytes_get_data(nm_wifi_ap_get_ssid(ap), &ssid_len);
+    ssid_len   = MIN(ssid_len, 32);
+    memcpy(name_buf, ssid_bytes, ssid_len);
+    name_buf[ssid_len] = '\0';
+
+    if (flags & NM_802_11_AP_FLAGS_PRIVACY)
+        kn_id.security = NM_IWD_NETWORK_SECURITY_WEP;
+
+    if (sec_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)
+        kn_id.security = NM_IWD_NETWORK_SECURITY_PSK;
+    else if (sec_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)
+        kn_id.security = NM_IWD_NETWORK_SECURITY_8021X;
+
+    /* Right now it's easier for us to do a name+security lookup than to use
+     * the Network.KnownNetwork property to look up by path.
+     */
+    data = g_hash_table_lookup(priv->known_networks, &kn_id);
+    if (data)
+        return data->mirror_connection;
+
+    /* We have no KnownNetwork for this AP, we're probably connecting to it for
+     * the first time.  This is not a usual/supported scenario so we don't need
+     * to bother too much about creating a great mirror connection, we don't
+     * even have any more information than the Name & Type properties on the
+     * Network interface.  This *should* never happen for an 8021x type network.
+     */
+    return mirror_connection(self, &kn_id, TRUE, NULL);
+}
+
+GDBusProxy *
+nm_iwd_manager_get_dbus_interface(NMIwdManager *self, const char *path, const char *name)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusInterface *     interface;
+
+    if (!priv->object_manager)
+        return NULL;
+
+    interface = g_dbus_object_manager_get_interface(priv->object_manager, path, name);
+
+    return interface ? G_DBUS_PROXY(interface) : NULL;
+}
+
+/*****************************************************************************/
+
+NM_DEFINE_SINGLETON_GETTER(NMIwdManager, nm_iwd_manager_get, NM_TYPE_IWD_MANAGER);
+
+static void
+nm_iwd_manager_init(NMIwdManager *self)
+{
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+
+    priv->manager = g_object_ref(NM_MANAGER_GET);
+    g_signal_connect(priv->manager, NM_MANAGER_DEVICE_ADDED, G_CALLBACK(device_added), self);
+    g_signal_connect(priv->manager, NM_MANAGER_DEVICE_REMOVED, G_CALLBACK(device_removed), self);
+
+    priv->settings = g_object_ref(NM_SETTINGS_GET);
+    g_signal_connect(priv->settings,
+                     NM_SETTINGS_SIGNAL_CONNECTION_REMOVED,
+                     G_CALLBACK(connection_removed),
+                     self);
+
+    priv->cancellable = g_cancellable_new();
+
+    priv->known_networks = g_hash_table_new_full((GHashFunc) known_network_id_hash,
+                                                 (GEqualFunc) known_network_id_equal,
+                                                 g_free,
+                                                 (GDestroyNotify) known_network_data_free);
+
+    prepare_object_manager(self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMIwdManager *       self = (NMIwdManager *) object;
+    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+
+    release_object_manager(self);
+
+    nm_clear_g_cancellable(&priv->cancellable);
+
+    if (priv->settings) {
+        g_signal_handlers_disconnect_by_data(priv->settings, self);
+        g_clear_object(&priv->settings);
+    }
+
+    /* This may trigger mirror connection removals so it happens
+     * after the g_signal_handlers_disconnect_by_data above.
+     */
+    nm_clear_pointer(&priv->known_networks, g_hash_table_destroy);
+
+    if (priv->manager) {
+        g_signal_handlers_disconnect_by_data(priv->manager, self);
+        g_clear_object(&priv->manager);
+    }
+
+    priv->last_agent_call_device = NULL;
+
+    G_OBJECT_CLASS(nm_iwd_manager_parent_class)->dispose(object);
+}
+
+static void
+nm_iwd_manager_class_init(NMIwdManagerClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->dispose = dispose;
+}
diff --git a/src/core/devices/wifi/nm-iwd-manager.h b/src/core/devices/wifi/nm-iwd-manager.h
new file mode 100644
index 00000000..466f67c7
--- /dev/null
+++ b/src/core/devices/wifi/nm-iwd-manager.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2017 Intel Corporation
+ */
+
+#ifndef __NETWORKMANAGER_IWD_MANAGER_H__
+#define __NETWORKMANAGER_IWD_MANAGER_H__
+
+#include "devices/nm-device.h"
+#include "nm-wifi-utils.h"
+#include "nm-wifi-ap.h"
+
+#define NM_IWD_BUS_TYPE G_BUS_TYPE_SYSTEM
+#define NM_IWD_SERVICE  "net.connman.iwd"
+
+#define NM_IWD_AGENT_MANAGER_INTERFACE "net.connman.iwd.AgentManager"
+#define NM_IWD_WIPHY_INTERFACE         "net.connman.iwd.Adapter"
+#define NM_IWD_DEVICE_INTERFACE        "net.connman.iwd.Device"
+#define NM_IWD_NETWORK_INTERFACE       "net.connman.iwd.Network"
+#define NM_IWD_AGENT_INTERFACE         "net.connman.iwd.Agent"
+#define NM_IWD_WSC_INTERFACE           "net.connman.iwd.WiFiSimpleConfiguration"
+#define NM_IWD_KNOWN_NETWORK_INTERFACE "net.connman.iwd.KnownNetwork"
+#define NM_IWD_SIGNAL_AGENT_INTERFACE  "net.connman.iwd.SignalLevelAgent"
+#define NM_IWD_AP_INTERFACE            "net.connman.iwd.AccessPoint"
+#define NM_IWD_ADHOC_INTERFACE         "net.connman.iwd.AdHoc"
+#define NM_IWD_STATION_INTERFACE       "net.connman.iwd.Station"
+
+#define NM_TYPE_IWD_MANAGER (nm_iwd_manager_get_type())
+#define NM_IWD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_IWD_MANAGER, NMIwdManager))
+#define NM_IWD_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_IWD_MANAGER, NMIwdManagerClass))
+#define NM_IS_IWD_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_IWD_MANAGER))
+#define NM_IS_IWD_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_IWD_MANAGER))
+#define NM_IWD_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_IWD_MANAGER, NMIwdManagerClass))
+
+typedef struct _NMIwdManager      NMIwdManager;
+typedef struct _NMIwdManagerClass NMIwdManagerClass;
+
+GType nm_iwd_manager_get_type(void);
+
+NMIwdManager *nm_iwd_manager_get(void);
+
+gboolean nm_iwd_manager_is_known_network(NMIwdManager *       self,
+                                         const char *         name,
+                                         NMIwdNetworkSecurity security);
+
+NMSettingsConnection *nm_iwd_manager_get_ap_mirror_connection(NMIwdManager *self, NMWifiAP *ap);
+
+GDBusProxy *
+nm_iwd_manager_get_dbus_interface(NMIwdManager *self, const char *path, const char *name);
+
+#endif /* __NETWORKMANAGER_IWD_MANAGER_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-ap.c b/src/core/devices/wifi/nm-wifi-ap.c
new file mode 100644
index 00000000..08fa10ec
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-ap.c
@@ -0,0 +1,1051 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2004 - 2017 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-wifi-ap.h"
+
+#include <stdlib.h>
+#include <linux/if_ether.h>
+
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device.h"
+#include "nm-core-internal.h"
+#include "nm-dbus-manager.h"
+#include "nm-glib-aux/nm-ref-string.h"
+#include "nm-setting-wireless.h"
+#include "nm-utils.h"
+#include "nm-wifi-utils.h"
+#include "platform/nm-platform.h"
+#include "supplicant/nm-supplicant-interface.h"
+
+#define PROTO_WPA "wpa"
+#define PROTO_RSN "rsn"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMWifiAP,
+                             PROP_FLAGS,
+                             PROP_WPA_FLAGS,
+                             PROP_RSN_FLAGS,
+                             PROP_SSID,
+                             PROP_FREQUENCY,
+                             PROP_HW_ADDRESS,
+                             PROP_MODE,
+                             PROP_MAX_BITRATE,
+                             PROP_STRENGTH,
+                             PROP_LAST_SEEN, );
+
+struct _NMWifiAPPrivate {
+    /* Scanned or cached values */
+    GBytes *    ssid;
+    char *      address;
+    NM80211Mode mode;
+    guint8      strength;
+    guint32     freq;        /* Frequency in MHz; ie 2412 (== 2.412 GHz) */
+    guint32     max_bitrate; /* Maximum bitrate of the AP in Kbit/s (ie 54000 Kb/s == 54Mbit/s) */
+
+    gint64
+        last_seen_msec; /* Timestamp when the AP was seen lastly (in nm_utils_get_monotonic_timestamp_*() scale).
+                         * Note that this value might be negative! */
+
+    NM80211ApFlags         flags;     /* General flags */
+    NM80211ApSecurityFlags wpa_flags; /* WPA-related flags */
+    NM80211ApSecurityFlags rsn_flags; /* RSN (WPA2) -related flags */
+
+    bool metered : 1;
+
+    /* Non-scanned attributes */
+    bool fake : 1;    /* Whether or not the AP is from a scan */
+    bool hotspot : 1; /* Whether the AP is a local device's hotspot network */
+};
+
+typedef struct _NMWifiAPPrivate NMWifiAPPrivate;
+
+struct _NMWifiAPClass {
+    NMDBusObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMWifiAP, nm_wifi_ap, NM_TYPE_DBUS_OBJECT)
+
+#define NM_WIFI_AP_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMWifiAP, NM_IS_WIFI_AP)
+
+/*****************************************************************************/
+
+GBytes *
+nm_wifi_ap_get_ssid(const NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->ssid;
+}
+
+gboolean
+nm_wifi_ap_set_ssid(NMWifiAP *ap, GBytes *ssid)
+{
+    NMWifiAPPrivate *priv;
+    gsize            l;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    if (!ssid) {
+        /* we don't clear the SSID, once we have it. We can only update
+         * it by a better value. */
+        return FALSE;
+    }
+
+    l = g_bytes_get_size(ssid);
+    if (l == 0 || l > 32)
+        g_return_val_if_reached(FALSE);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (ssid == priv->ssid)
+        return FALSE;
+    if (priv->ssid && g_bytes_equal(ssid, priv->ssid))
+        return FALSE;
+
+    g_bytes_ref(ssid);
+    nm_clear_pointer(&priv->ssid, g_bytes_unref);
+    priv->ssid = ssid;
+
+    _notify(ap, PROP_SSID);
+    return TRUE;
+}
+
+static gboolean
+nm_wifi_ap_set_flags(NMWifiAP *ap, NM80211ApFlags flags)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->flags != flags) {
+        priv->flags = flags;
+        _notify(ap, PROP_FLAGS);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+nm_wifi_ap_set_wpa_flags(NMWifiAP *ap, NM80211ApSecurityFlags flags)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->wpa_flags != flags) {
+        priv->wpa_flags = flags;
+        _notify(ap, PROP_WPA_FLAGS);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+static gboolean
+nm_wifi_ap_set_rsn_flags(NMWifiAP *ap, NM80211ApSecurityFlags flags)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->rsn_flags != flags) {
+        priv->rsn_flags = flags;
+        _notify(ap, PROP_RSN_FLAGS);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+const char *
+nm_wifi_ap_get_address(const NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->address;
+}
+
+gboolean
+nm_wifi_ap_set_address_bin(NMWifiAP *ap, const NMEtherAddr *addr)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    nm_assert(addr);
+
+    if (!priv->address || !nm_utils_hwaddr_matches(addr, ETH_ALEN, priv->address, -1)) {
+        g_free(priv->address);
+        priv->address = nm_utils_hwaddr_ntoa(addr, ETH_ALEN);
+        _notify(ap, PROP_HW_ADDRESS);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_wifi_ap_set_address(NMWifiAP *ap, const char *addr)
+{
+    NMEtherAddr addr_buf;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+    if (!addr || !nm_utils_hwaddr_aton(addr, &addr_buf, sizeof(addr_buf)))
+        g_return_val_if_reached(FALSE);
+
+    return nm_wifi_ap_set_address_bin(ap, &addr_buf);
+}
+
+NM80211Mode
+nm_wifi_ap_get_mode(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), NM_802_11_MODE_UNKNOWN);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->mode;
+}
+
+static gboolean
+nm_wifi_ap_set_mode(NMWifiAP *ap, NM80211Mode mode)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    nm_assert(NM_IN_SET(mode,
+                        NM_802_11_MODE_UNKNOWN,
+                        NM_802_11_MODE_ADHOC,
+                        NM_802_11_MODE_INFRA,
+                        NM_802_11_MODE_MESH));
+
+    if (priv->mode != mode) {
+        priv->mode = mode;
+        _notify(ap, PROP_MODE);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_wifi_ap_is_hotspot(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->hotspot;
+}
+
+gint8
+nm_wifi_ap_get_strength(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->strength;
+}
+
+gboolean
+nm_wifi_ap_set_strength(NMWifiAP *ap, gint8 strength)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->strength != strength) {
+        priv->strength = strength;
+        _notify(ap, PROP_STRENGTH);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+guint32
+nm_wifi_ap_get_freq(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->freq;
+}
+
+gboolean
+nm_wifi_ap_set_freq(NMWifiAP *ap, guint32 freq)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->freq != freq) {
+        priv->freq = freq;
+        _notify(ap, PROP_FREQUENCY);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+guint32
+nm_wifi_ap_get_max_bitrate(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0);
+    g_return_val_if_fail(nm_dbus_object_is_exported(NM_DBUS_OBJECT(ap)), 0);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->max_bitrate;
+}
+
+gboolean
+nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate)
+{
+    NMWifiAPPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->max_bitrate != bitrate) {
+        priv->max_bitrate = bitrate;
+        _notify(ap, PROP_MAX_BITRATE);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_wifi_ap_get_fake(const NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->fake;
+}
+
+gboolean
+nm_wifi_ap_set_fake(NMWifiAP *ap, gboolean fake)
+{
+    NMWifiAPPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->fake != !!fake) {
+        priv->fake = fake;
+        return TRUE;
+    }
+    return FALSE;
+}
+
+NM80211ApFlags
+nm_wifi_ap_get_flags(const NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), NM_802_11_AP_FLAGS_NONE);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->flags;
+}
+
+static gboolean
+nm_wifi_ap_set_last_seen(NMWifiAP *ap, gint32 last_seen_msec)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->last_seen_msec != last_seen_msec) {
+        priv->last_seen_msec = last_seen_msec;
+        _notify(ap, PROP_LAST_SEEN);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+gboolean
+nm_wifi_ap_get_metered(const NMWifiAP *self)
+{
+    return NM_WIFI_AP_GET_PRIVATE(self)->metered;
+}
+
+NM80211ApSecurityFlags
+nm_wifi_ap_get_wpa_flags(const NMWifiAP *self)
+{
+    return NM_WIFI_AP_GET_PRIVATE(self)->wpa_flags;
+}
+
+NM80211ApSecurityFlags
+nm_wifi_ap_get_rsn_flags(const NMWifiAP *self)
+{
+    return NM_WIFI_AP_GET_PRIVATE(self)->rsn_flags;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_wifi_ap_update_from_properties(NMWifiAP *ap, const NMSupplicantBssInfo *bss_info)
+{
+    NMWifiAPPrivate *priv;
+    gboolean         changed = FALSE;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+    g_return_val_if_fail(bss_info, FALSE);
+    nm_assert(NM_IS_REF_STRING(bss_info->bss_path));
+
+    priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    nm_assert(!ap->_supplicant_path || ap->_supplicant_path == bss_info->bss_path);
+
+    g_object_freeze_notify(G_OBJECT(ap));
+
+    if (!ap->_supplicant_path) {
+        ap->_supplicant_path = nm_ref_string_ref(bss_info->bss_path);
+        changed              = TRUE;
+    }
+
+    changed |= nm_wifi_ap_set_flags(ap, bss_info->ap_flags);
+    changed |= nm_wifi_ap_set_mode(ap, bss_info->mode);
+    changed |= nm_wifi_ap_set_strength(ap, bss_info->signal_percent);
+    changed |= nm_wifi_ap_set_freq(ap, bss_info->frequency);
+    changed |= nm_wifi_ap_set_ssid(ap, bss_info->ssid);
+
+    if (bss_info->bssid_valid)
+        changed |= nm_wifi_ap_set_address_bin(ap, &bss_info->bssid);
+    else {
+        /* we don't actually clear the value. */
+    }
+
+    changed |= nm_wifi_ap_set_max_bitrate(ap, bss_info->max_rate);
+
+    if (priv->metered != bss_info->metered) {
+        priv->metered = bss_info->metered;
+        changed       = TRUE;
+    }
+
+    changed |= nm_wifi_ap_set_wpa_flags(ap, bss_info->wpa_flags);
+    changed |= nm_wifi_ap_set_rsn_flags(ap, bss_info->rsn_flags);
+
+    changed |= nm_wifi_ap_set_last_seen(ap, bss_info->last_seen_msec);
+
+    changed |= nm_wifi_ap_set_fake(ap, FALSE);
+
+    g_object_thaw_notify(G_OBJECT(ap));
+
+    return changed;
+}
+
+static gboolean
+has_proto(NMSettingWirelessSecurity *sec, const char *proto)
+{
+    guint32 num_protos = nm_setting_wireless_security_get_num_protos(sec);
+    guint32 i;
+
+    if (num_protos == 0)
+        return TRUE; /* interpret no protos as "all" */
+
+    for (i = 0; i < num_protos; i++) {
+        if (!strcmp(nm_setting_wireless_security_get_proto(sec, i), proto))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+static void
+add_pair_ciphers(NMWifiAP *ap, NMSettingWirelessSecurity *sec)
+{
+    NMWifiAPPrivate *      priv  = NM_WIFI_AP_GET_PRIVATE(ap);
+    guint32                num   = nm_setting_wireless_security_get_num_pairwise(sec);
+    NM80211ApSecurityFlags flags = NM_802_11_AP_SEC_NONE;
+    guint32                i;
+
+    /* If no ciphers are specified, that means "all" WPA ciphers */
+    if (num == 0) {
+        flags |= NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP;
+    } else {
+        for (i = 0; i < num; i++) {
+            const char *cipher = nm_setting_wireless_security_get_pairwise(sec, i);
+
+            if (!strcmp(cipher, "tkip"))
+                flags |= NM_802_11_AP_SEC_PAIR_TKIP;
+            else if (!strcmp(cipher, "ccmp"))
+                flags |= NM_802_11_AP_SEC_PAIR_CCMP;
+        }
+    }
+
+    if (has_proto(sec, PROTO_WPA))
+        nm_wifi_ap_set_wpa_flags(ap, priv->wpa_flags | flags);
+    if (has_proto(sec, PROTO_RSN))
+        nm_wifi_ap_set_rsn_flags(ap, priv->rsn_flags | flags);
+}
+
+static void
+add_group_ciphers(NMWifiAP *ap, NMSettingWirelessSecurity *sec)
+{
+    NMWifiAPPrivate *      priv  = NM_WIFI_AP_GET_PRIVATE(ap);
+    guint32                num   = nm_setting_wireless_security_get_num_groups(sec);
+    NM80211ApSecurityFlags flags = NM_802_11_AP_SEC_NONE;
+    guint32                i;
+
+    /* If no ciphers are specified, that means "all" WPA ciphers */
+    if (num == 0) {
+        flags |= NM_802_11_AP_SEC_GROUP_TKIP | NM_802_11_AP_SEC_GROUP_CCMP;
+    } else {
+        for (i = 0; i < num; i++) {
+            const char *cipher = nm_setting_wireless_security_get_group(sec, i);
+
+            if (!strcmp(cipher, "wep40"))
+                flags |= NM_802_11_AP_SEC_GROUP_WEP40;
+            else if (!strcmp(cipher, "wep104"))
+                flags |= NM_802_11_AP_SEC_GROUP_WEP104;
+            else if (!strcmp(cipher, "tkip"))
+                flags |= NM_802_11_AP_SEC_GROUP_TKIP;
+            else if (!strcmp(cipher, "ccmp"))
+                flags |= NM_802_11_AP_SEC_GROUP_CCMP;
+        }
+    }
+
+    if (has_proto(sec, PROTO_WPA))
+        nm_wifi_ap_set_wpa_flags(ap, priv->wpa_flags | flags);
+    if (has_proto(sec, PROTO_RSN))
+        nm_wifi_ap_set_rsn_flags(ap, priv->rsn_flags | flags);
+}
+
+const char *
+nm_wifi_ap_to_string(const NMWifiAP *self, char *str_buf, gulong buf_len, gint64 now_msec)
+{
+    const NMWifiAPPrivate *priv;
+    const char *           supplicant_id = "-";
+    const char *           export_path;
+    guint32                chan;
+    gs_free char *         ssid_to_free = NULL;
+    char                   str_buf_ts[100];
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(self), NULL);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(self);
+
+    chan = nm_utils_wifi_freq_to_channel(priv->freq);
+    if (self->_supplicant_path)
+        supplicant_id = strrchr(self->_supplicant_path->str, '/') ?: supplicant_id;
+
+    export_path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self));
+    if (export_path)
+        export_path = strrchr(export_path, '/') ?: export_path;
+    else
+        export_path = "/";
+
+    nm_utils_get_monotonic_timestamp_msec_cached(&now_msec);
+
+    g_snprintf(str_buf,
+               buf_len,
+               "%17s %-35s [ %c %3u %3u%% %c%c %c%c W:%04X R:%04X ] %s sup:%s [nm:%s]",
+               priv->address ?: "(none)",
+               (ssid_to_free = _nm_utils_ssid_to_string(priv->ssid)),
+               (priv->mode == NM_802_11_MODE_ADHOC
+                    ? '*'
+                    : (priv->hotspot
+                           ? '#'
+                           : (priv->fake ? 'f' : (priv->mode == NM_802_11_MODE_MESH ? 'm' : 'a')))),
+               chan,
+               priv->strength,
+               priv->flags & NM_802_11_AP_FLAGS_PRIVACY ? 'P' : '_',
+               priv->metered ? 'M' : '_',
+               priv->flags & NM_802_11_AP_FLAGS_WPS ? 'W' : '_',
+               priv->flags & NM_802_11_AP_FLAGS_WPS_PIN
+                   ? 'p'
+                   : (priv->flags & NM_802_11_AP_FLAGS_WPS_PBC ? '#' : '_'),
+               priv->wpa_flags & 0xFFFF,
+               priv->rsn_flags & 0xFFFF,
+               priv->last_seen_msec != G_MININT64
+                   ? nm_sprintf_buf(str_buf_ts,
+                                    "%3u.%03us",
+                                    (guint)((now_msec - priv->last_seen_msec) / 1000),
+                                    (guint)((now_msec - priv->last_seen_msec) % 1000))
+                   : "        ",
+               supplicant_id,
+               export_path);
+    return str_buf;
+}
+
+static guint
+freq_to_band(guint32 freq)
+{
+    if (freq >= 4915 && freq <= 5825)
+        return 5;
+    else if (freq >= 2412 && freq <= 2484)
+        return 2;
+    return 0;
+}
+
+gboolean
+nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection)
+{
+    NMWifiAPPrivate *          priv;
+    NMSettingWireless *        s_wireless;
+    NMSettingWirelessSecurity *s_wireless_sec;
+    GBytes *                   ssid;
+    const char *               mode;
+    const char *               band;
+    const char *               bssid;
+    guint32                    channel;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(self);
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    if (s_wireless == NULL)
+        return FALSE;
+
+    ssid = nm_setting_wireless_get_ssid(s_wireless);
+    if (ssid != priv->ssid) {
+        if (!ssid || !priv->ssid)
+            return FALSE;
+        if (!g_bytes_equal(ssid, priv->ssid))
+            return FALSE;
+    }
+
+    bssid = nm_setting_wireless_get_bssid(s_wireless);
+    if (bssid && (!priv->address || !nm_utils_hwaddr_matches(bssid, -1, priv->address, -1)))
+        return FALSE;
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (mode) {
+        if (!strcmp(mode, "infrastructure") && (priv->mode != NM_802_11_MODE_INFRA))
+            return FALSE;
+        if (!strcmp(mode, "adhoc") && (priv->mode != NM_802_11_MODE_ADHOC))
+            return FALSE;
+        if (!strcmp(mode, "ap") && (priv->mode != NM_802_11_MODE_INFRA || priv->hotspot != TRUE))
+            return FALSE;
+        if (!strcmp(mode, "mesh") && (priv->mode != NM_802_11_MODE_MESH))
+            return FALSE;
+    }
+
+    band = nm_setting_wireless_get_band(s_wireless);
+    if (band) {
+        guint ap_band = freq_to_band(priv->freq);
+
+        if (!strcmp(band, "a") && ap_band != 5)
+            return FALSE;
+        else if (!strcmp(band, "bg") && ap_band != 2)
+            return FALSE;
+    }
+
+    channel = nm_setting_wireless_get_channel(s_wireless);
+    if (channel) {
+        guint32 ap_chan = nm_utils_wifi_freq_to_channel(priv->freq);
+
+        if (channel != ap_chan)
+            return FALSE;
+    }
+
+    s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+
+    return nm_setting_wireless_ap_security_compatible(s_wireless,
+                                                      s_wireless_sec,
+                                                      priv->flags,
+                                                      priv->wpa_flags,
+                                                      priv->rsn_flags,
+                                                      priv->mode);
+}
+
+gboolean
+nm_wifi_ap_complete_connection(NMWifiAP *    self,
+                               NMConnection *connection,
+                               gboolean      lock_bssid,
+                               GError **     error)
+{
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self);
+
+    g_return_val_if_fail(connection != NULL, FALSE);
+
+    return nm_wifi_utils_complete_connection(priv->ssid,
+                                             priv->address,
+                                             priv->mode,
+                                             priv->freq,
+                                             priv->flags,
+                                             priv->wpa_flags,
+                                             priv->rsn_flags,
+                                             connection,
+                                             lock_bssid,
+                                             error);
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMWifiAP *       self = NM_WIFI_AP(object);
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_FLAGS:
+        g_value_set_uint(value, priv->flags);
+        break;
+    case PROP_WPA_FLAGS:
+        g_value_set_uint(value, priv->wpa_flags);
+        break;
+    case PROP_RSN_FLAGS:
+        g_value_set_uint(value, priv->rsn_flags);
+        break;
+    case PROP_SSID:
+        g_value_take_variant(value, nm_utils_gbytes_to_variant_ay(priv->ssid));
+        break;
+    case PROP_FREQUENCY:
+        g_value_set_uint(value, priv->freq);
+        break;
+    case PROP_HW_ADDRESS:
+        g_value_set_string(value, priv->address);
+        break;
+    case PROP_MODE:
+        g_value_set_uint(value, priv->mode);
+        break;
+    case PROP_MAX_BITRATE:
+        g_value_set_uint(value, priv->max_bitrate);
+        break;
+    case PROP_STRENGTH:
+        g_value_set_uchar(value, priv->strength);
+        break;
+    case PROP_LAST_SEEN:
+        g_value_set_int(value,
+                        priv->last_seen_msec != G_MININT64 ? (int) NM_MAX(
+                            nm_utils_monotonic_timestamp_as_boottime(priv->last_seen_msec,
+                                                                     NM_UTILS_NSEC_PER_MSEC)
+                                / 1000,
+                            1)
+                                                           : -1);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_wifi_ap_init(NMWifiAP *self)
+{
+    NMWifiAPPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_WIFI_AP, NMWifiAPPrivate);
+
+    self->_priv = priv;
+
+    c_list_init(&self->aps_lst);
+
+    priv->mode           = NM_802_11_MODE_INFRA;
+    priv->flags          = NM_802_11_AP_FLAGS_NONE;
+    priv->wpa_flags      = NM_802_11_AP_SEC_NONE;
+    priv->rsn_flags      = NM_802_11_AP_SEC_NONE;
+    priv->last_seen_msec = G_MININT64;
+}
+
+NMWifiAP *
+nm_wifi_ap_new_from_properties(const NMSupplicantBssInfo *bss_info)
+{
+    NMWifiAP *ap;
+
+    ap = g_object_new(NM_TYPE_WIFI_AP, NULL);
+    nm_wifi_ap_update_from_properties(ap, bss_info);
+    return ap;
+}
+
+NMWifiAP *
+nm_wifi_ap_new_fake_from_connection(NMConnection *connection)
+{
+    NMWifiAP *                 ap;
+    NMWifiAPPrivate *          priv;
+    NMSettingWireless *        s_wireless;
+    NMSettingWirelessSecurity *s_wireless_sec;
+    const char *               mode, *band, *key_mgmt;
+    guint32                    channel;
+    NM80211ApSecurityFlags     flags;
+    gboolean                   psk = FALSE, eap = FALSE, adhoc = FALSE;
+
+    g_return_val_if_fail(connection != NULL, NULL);
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    g_return_val_if_fail(s_wireless != NULL, NULL);
+
+    ap         = g_object_new(NM_TYPE_WIFI_AP, NULL);
+    priv       = NM_WIFI_AP_GET_PRIVATE(ap);
+    priv->fake = TRUE;
+
+    nm_wifi_ap_set_ssid(ap, nm_setting_wireless_get_ssid(s_wireless));
+
+    // FIXME: bssid too?
+
+    mode = nm_setting_wireless_get_mode(s_wireless);
+    if (mode) {
+        if (!strcmp(mode, "infrastructure"))
+            nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA);
+        else if (!strcmp(mode, "adhoc")) {
+            nm_wifi_ap_set_mode(ap, NM_802_11_MODE_ADHOC);
+            adhoc = TRUE;
+        } else if (!strcmp(mode, "mesh"))
+            nm_wifi_ap_set_mode(ap, NM_802_11_MODE_MESH);
+        else if (!strcmp(mode, "ap")) {
+            nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA);
+            NM_WIFI_AP_GET_PRIVATE(ap)->hotspot = TRUE;
+        } else
+            goto error;
+    } else {
+        nm_wifi_ap_set_mode(ap, NM_802_11_MODE_INFRA);
+    }
+
+    band    = nm_setting_wireless_get_band(s_wireless);
+    channel = nm_setting_wireless_get_channel(s_wireless);
+
+    if (band && channel) {
+        guint32 freq = nm_utils_wifi_channel_to_freq(channel, band);
+
+        if (freq == 0)
+            goto error;
+
+        nm_wifi_ap_set_freq(ap, freq);
+    }
+
+    s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+    /* Assume presence of a security setting means the AP is encrypted */
+    if (!s_wireless_sec)
+        goto done;
+
+    key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wireless_sec);
+
+    /* Everything below here uses encryption */
+    nm_wifi_ap_set_flags(ap, priv->flags | NM_802_11_AP_FLAGS_PRIVACY);
+
+    /* Static & Dynamic WEP */
+    if (!strcmp(key_mgmt, "none") || !strcmp(key_mgmt, "ieee8021x"))
+        goto done;
+
+    psk = nm_streq(key_mgmt, "wpa-psk");
+    eap = nm_streq(key_mgmt, "wpa-eap") || nm_streq(key_mgmt, "wpa-eap-suite-b-192");
+    if (!adhoc && (psk || eap)) {
+        if (has_proto(s_wireless_sec, PROTO_WPA)) {
+            flags = priv->wpa_flags
+                    | (eap ? NM_802_11_AP_SEC_KEY_MGMT_802_1X : NM_802_11_AP_SEC_KEY_MGMT_PSK);
+            nm_wifi_ap_set_wpa_flags(ap, flags);
+        }
+        if (has_proto(s_wireless_sec, PROTO_RSN)) {
+            flags = priv->rsn_flags
+                    | (eap ? NM_802_11_AP_SEC_KEY_MGMT_802_1X : NM_802_11_AP_SEC_KEY_MGMT_PSK);
+            nm_wifi_ap_set_rsn_flags(ap, flags);
+        }
+
+        add_pair_ciphers(ap, s_wireless_sec);
+        add_group_ciphers(ap, s_wireless_sec);
+    } else if (adhoc && psk) {
+        /* Ad-Hoc has special requirements: proto=RSN, pairwise=CCMP and
+         * group=CCMP.
+         */
+        flags = priv->wpa_flags | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+
+        /* Clear ciphers; only CCMP is supported */
+        flags &= ~(NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104
+                   | NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_WEP40
+                   | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_GROUP_TKIP);
+
+        flags |= NM_802_11_AP_SEC_PAIR_CCMP;
+        flags |= NM_802_11_AP_SEC_GROUP_CCMP;
+        nm_wifi_ap_set_rsn_flags(ap, flags);
+
+        /* Don't use Ad-Hoc WPA (WPA-none) anymore */
+        nm_wifi_ap_set_wpa_flags(ap, NM_802_11_AP_SEC_NONE);
+    }
+done:
+    return ap;
+
+error:
+    g_object_unref(ap);
+    return NULL;
+}
+
+static void
+finalize(GObject *object)
+{
+    NMWifiAP *       self = NM_WIFI_AP(object);
+    NMWifiAPPrivate *priv = NM_WIFI_AP_GET_PRIVATE(self);
+
+    nm_assert(!self->wifi_device);
+    nm_assert(c_list_is_empty(&self->aps_lst));
+
+    nm_ref_string_unref(self->_supplicant_path);
+    if (priv->ssid)
+        g_bytes_unref(priv->ssid);
+    g_free(priv->address);
+
+    G_OBJECT_CLASS(nm_wifi_ap_parent_class)->finalize(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_access_point = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_ACCESS_POINT,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Flags", "u", NM_WIFI_AP_FLAGS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("WpaFlags", "u", NM_WIFI_AP_WPA_FLAGS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("RsnFlags", "u", NM_WIFI_AP_RSN_FLAGS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Ssid", "ay", NM_WIFI_AP_SSID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Frequency",
+                                                             "u",
+                                                             NM_WIFI_AP_FREQUENCY),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_WIFI_AP_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_WIFI_AP_MODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("MaxBitrate",
+                                                             "u",
+                                                             NM_WIFI_AP_MAX_BITRATE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Strength", "y", NM_WIFI_AP_STRENGTH),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("LastSeen",
+                                                             "i",
+                                                             NM_WIFI_AP_LAST_SEEN), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_wifi_ap_class_init(NMWifiAPClass *ap_class)
+{
+#define ALL_SEC_FLAGS                                                                            \
+    (NM_802_11_AP_SEC_NONE | NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104          \
+     | NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_WEP40    \
+     | NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_GROUP_TKIP | NM_802_11_AP_SEC_GROUP_CCMP \
+     | NM_802_11_AP_SEC_KEY_MGMT_PSK | NM_802_11_AP_SEC_KEY_MGMT_802_1X                          \
+     | NM_802_11_AP_SEC_KEY_MGMT_SAE | NM_802_11_AP_SEC_KEY_MGMT_OWE                             \
+     | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM | NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)
+
+    GObjectClass *     object_class      = G_OBJECT_CLASS(ap_class);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(ap_class);
+
+    g_type_class_add_private(object_class, sizeof(NMWifiAPPrivate));
+
+    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH_ACCESS_POINT);
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_access_point);
+
+    object_class->get_property = get_property;
+    object_class->finalize     = finalize;
+
+    obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_WIFI_AP_FLAGS,
+                                                   "",
+                                                   "",
+                                                   NM_802_11_AP_FLAGS_NONE,
+                                                   NM_802_11_AP_FLAGS_PRIVACY,
+                                                   NM_802_11_AP_FLAGS_NONE,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_WPA_FLAGS] = g_param_spec_uint(NM_WIFI_AP_WPA_FLAGS,
+                                                       "",
+                                                       "",
+                                                       NM_802_11_AP_SEC_NONE,
+                                                       ALL_SEC_FLAGS,
+                                                       NM_802_11_AP_SEC_NONE,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_RSN_FLAGS] = g_param_spec_uint(NM_WIFI_AP_RSN_FLAGS,
+                                                       "",
+                                                       "",
+                                                       NM_802_11_AP_SEC_NONE,
+                                                       ALL_SEC_FLAGS,
+                                                       NM_802_11_AP_SEC_NONE,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SSID] = g_param_spec_variant(NM_WIFI_AP_SSID,
+                                                     "",
+                                                     "",
+                                                     G_VARIANT_TYPE("ay"),
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_FREQUENCY] = g_param_spec_uint(NM_WIFI_AP_FREQUENCY,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       10000,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_HW_ADDRESS] =
+        g_param_spec_string(NM_WIFI_AP_HW_ADDRESS,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MODE] = g_param_spec_uint(NM_WIFI_AP_MODE,
+                                                  "",
+                                                  "",
+                                                  NM_802_11_MODE_ADHOC,
+                                                  NM_802_11_MODE_INFRA,
+                                                  NM_802_11_MODE_INFRA,
+                                                  G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MAX_BITRATE] = g_param_spec_uint(NM_WIFI_AP_MAX_BITRATE,
+                                                         "",
+                                                         "",
+                                                         0,
+                                                         G_MAXUINT16,
+                                                         0,
+                                                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_STRENGTH] = g_param_spec_uchar(NM_WIFI_AP_STRENGTH,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       G_MAXINT8,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LAST_SEEN] = g_param_spec_int(NM_WIFI_AP_LAST_SEEN,
+                                                      "",
+                                                      "",
+                                                      -1,
+                                                      G_MAXINT,
+                                                      -1,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+const char **
+nm_wifi_aps_get_paths(const CList *aps_lst_head, gboolean include_without_ssid)
+{
+    NMWifiAP *   ap;
+    gsize        i, n;
+    const char **list;
+    const char * path;
+
+    n    = c_list_length(aps_lst_head);
+    list = g_new(const char *, n + 1);
+
+    i = 0;
+    if (n > 0) {
+        c_list_for_each_entry (ap, aps_lst_head, aps_lst) {
+            nm_assert(i < n);
+            if (!include_without_ssid && !nm_wifi_ap_get_ssid(ap))
+                continue;
+
+            path = nm_dbus_object_get_path(NM_DBUS_OBJECT(ap));
+            nm_assert(path);
+
+            list[i++] = path;
+        }
+        nm_assert(i <= n);
+        nm_assert(!include_without_ssid || i == n);
+    }
+    list[i] = NULL;
+    return list;
+}
+
+NMWifiAP *
+nm_wifi_aps_find_first_compatible(const CList *aps_lst_head, NMConnection *connection)
+{
+    NMWifiAP *ap;
+
+    g_return_val_if_fail(connection, NULL);
+
+    c_list_for_each_entry (ap, aps_lst_head, aps_lst) {
+        if (nm_wifi_ap_check_compatible(ap, connection))
+            return ap;
+    }
+    return NULL;
+}
+
+/*****************************************************************************/
+
+NMWifiAP *
+nm_wifi_ap_lookup_for_device(NMDevice *device, const char *exported_path)
+{
+    NMWifiAP *ap;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), NULL);
+
+    ap = nm_dbus_manager_lookup_object(nm_dbus_object_get_manager(NM_DBUS_OBJECT(device)),
+                                       exported_path);
+    if (!ap || !NM_IS_WIFI_AP(ap) || ap->wifi_device != device)
+        return NULL;
+
+    return ap;
+}
diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h
new file mode 100644
index 00000000..bdd72415
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-ap.h
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2004 - 2017 Red Hat, Inc.
+ * Copyright (C) 2006 - 2008 Novell, Inc.
+ */
+
+#ifndef __NM_WIFI_AP_H__
+#define __NM_WIFI_AP_H__
+
+#include "nm-dbus-object.h"
+#include "nm-dbus-interface.h"
+#include "nm-connection.h"
+
+#define NM_TYPE_WIFI_AP            (nm_wifi_ap_get_type())
+#define NM_WIFI_AP(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_AP, NMWifiAP))
+#define NM_WIFI_AP_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_AP, NMWifiAPClass))
+#define NM_IS_WIFI_AP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_AP))
+#define NM_IS_WIFI_AP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_AP))
+#define NM_WIFI_AP_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_AP, NMWifiAPClass))
+
+#define NM_WIFI_AP_FLAGS       "flags"
+#define NM_WIFI_AP_WPA_FLAGS   "wpa-flags"
+#define NM_WIFI_AP_RSN_FLAGS   "rsn-flags"
+#define NM_WIFI_AP_SSID        "ssid"
+#define NM_WIFI_AP_FREQUENCY   "frequency"
+#define NM_WIFI_AP_HW_ADDRESS  "hw-address"
+#define NM_WIFI_AP_MODE        "mode"
+#define NM_WIFI_AP_MAX_BITRATE "max-bitrate"
+#define NM_WIFI_AP_STRENGTH    "strength"
+#define NM_WIFI_AP_LAST_SEEN   "last-seen"
+
+typedef struct {
+    NMDBusObject             parent;
+    NMDevice *               wifi_device;
+    CList                    aps_lst;
+    NMRefString *            _supplicant_path;
+    struct _NMWifiAPPrivate *_priv;
+} NMWifiAP;
+
+struct _NMSupplicantBssInfo;
+
+typedef struct _NMWifiAPClass NMWifiAPClass;
+
+GType nm_wifi_ap_get_type(void);
+
+NMWifiAP *nm_wifi_ap_new_from_properties(const struct _NMSupplicantBssInfo *bss_info);
+NMWifiAP *nm_wifi_ap_new_fake_from_connection(NMConnection *connection);
+
+gboolean nm_wifi_ap_update_from_properties(NMWifiAP *                         ap,
+                                           const struct _NMSupplicantBssInfo *bss_info);
+
+gboolean nm_wifi_ap_check_compatible(NMWifiAP *self, NMConnection *connection);
+
+gboolean nm_wifi_ap_complete_connection(NMWifiAP *    self,
+                                        NMConnection *connection,
+                                        gboolean      lock_bssid,
+                                        GError **     error);
+
+static inline NMRefString *
+nm_wifi_ap_get_supplicant_path(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), NULL);
+
+    return ap->_supplicant_path;
+}
+
+GBytes *               nm_wifi_ap_get_ssid(const NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_ssid(NMWifiAP *ap, GBytes *ssid);
+const char *           nm_wifi_ap_get_address(const NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_address(NMWifiAP *ap, const char *addr);
+gboolean               nm_wifi_ap_set_address_bin(NMWifiAP *ap, const NMEtherAddr *addr);
+NM80211Mode            nm_wifi_ap_get_mode(NMWifiAP *ap);
+gboolean               nm_wifi_ap_is_hotspot(NMWifiAP *ap);
+gint8                  nm_wifi_ap_get_strength(NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_strength(NMWifiAP *ap, gint8 strength);
+guint32                nm_wifi_ap_get_freq(NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_freq(NMWifiAP *ap, guint32 freq);
+guint32                nm_wifi_ap_get_max_bitrate(NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate);
+gboolean               nm_wifi_ap_get_fake(const NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_fake(NMWifiAP *ap, gboolean fake);
+NM80211ApFlags         nm_wifi_ap_get_flags(const NMWifiAP *self);
+gboolean               nm_wifi_ap_get_metered(const NMWifiAP *self);
+NM80211ApSecurityFlags nm_wifi_ap_get_wpa_flags(const NMWifiAP *self);
+NM80211ApSecurityFlags nm_wifi_ap_get_rsn_flags(const NMWifiAP *self);
+
+const char *
+nm_wifi_ap_to_string(const NMWifiAP *self, char *str_buf, gulong buf_len, gint64 now_msec);
+
+const char **nm_wifi_aps_get_paths(const CList *aps_lst_head, gboolean include_without_ssid);
+
+NMWifiAP *nm_wifi_aps_find_first_compatible(const CList *aps_lst_head, NMConnection *connection);
+
+NMWifiAP *nm_wifi_ap_lookup_for_device(NMDevice *device, const char *exported_path);
+
+#endif /* __NM_WIFI_AP_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-common.c b/src/core/devices/wifi/nm-wifi-common.c
new file mode 100644
index 00000000..c715c07c
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-common.c
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-wifi-common.h"
+
+#include "devices/nm-device.h"
+#include "nm-wifi-ap.h"
+#include "nm-device-wifi.h"
+#include "nm-dbus-manager.h"
+
+#if WITH_IWD
+    #include "nm-device-iwd.h"
+#endif
+
+/*****************************************************************************/
+
+void
+nm_device_wifi_emit_signal_access_point(NMDevice *device,
+                                        NMWifiAP *ap,
+                                        gboolean  is_added /* or else is_removed */)
+{
+    nm_dbus_object_emit_signal(NM_DBUS_OBJECT(device),
+                               &nm_interface_info_device_wireless,
+                               is_added ? &nm_signal_info_wireless_access_point_added
+                                        : &nm_signal_info_wireless_access_point_removed,
+                               "(o)",
+                               nm_dbus_object_get_path(NM_DBUS_OBJECT(ap)));
+}
+
+/*****************************************************************************/
+
+static const CList *
+_dispatch_get_aps(NMDevice *device)
+{
+#if WITH_IWD
+    if (NM_IS_DEVICE_IWD(device))
+        return _nm_device_iwd_get_aps(NM_DEVICE_IWD(device));
+#endif
+    return _nm_device_wifi_get_aps(NM_DEVICE_WIFI(device));
+}
+
+static void
+_dispatch_request_scan(NMDevice *device, GVariant *options, GDBusMethodInvocation *invocation)
+{
+#if WITH_IWD
+    if (NM_IS_DEVICE_IWD(device)) {
+        _nm_device_iwd_request_scan(NM_DEVICE_IWD(device), options, invocation);
+        return;
+    }
+#endif
+    _nm_device_wifi_request_scan(NM_DEVICE_WIFI(device), options, invocation);
+}
+
+static void
+impl_device_wifi_get_access_points(NMDBusObject *                     obj,
+                                   const NMDBusInterfaceInfoExtended *interface_info,
+                                   const NMDBusMethodInfoExtended *   method_info,
+                                   GDBusConnection *                  connection,
+                                   const char *                       sender,
+                                   GDBusMethodInvocation *            invocation,
+                                   GVariant *                         parameters)
+{
+    gs_free const char **list = NULL;
+    GVariant *           v;
+    const CList *        all_aps;
+
+    /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */
+
+    all_aps = _dispatch_get_aps(NM_DEVICE(obj));
+    list    = nm_wifi_aps_get_paths(all_aps, FALSE);
+    v       = g_variant_new_objv(list, -1);
+    g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&v, 1));
+}
+
+static void
+impl_device_wifi_get_all_access_points(NMDBusObject *                     obj,
+                                       const NMDBusInterfaceInfoExtended *interface_info,
+                                       const NMDBusMethodInfoExtended *   method_info,
+                                       GDBusConnection *                  connection,
+                                       const char *                       sender,
+                                       GDBusMethodInvocation *            invocation,
+                                       GVariant *                         parameters)
+{
+    gs_free const char **list = NULL;
+    GVariant *           v;
+    const CList *        all_aps;
+
+    /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */
+
+    all_aps = _dispatch_get_aps(NM_DEVICE(obj));
+    list    = nm_wifi_aps_get_paths(all_aps, TRUE);
+    v       = g_variant_new_objv(list, -1);
+    g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&v, 1));
+}
+
+static void
+impl_device_wifi_request_scan(NMDBusObject *                     obj,
+                              const NMDBusInterfaceInfoExtended *interface_info,
+                              const NMDBusMethodInfoExtended *   method_info,
+                              GDBusConnection *                  connection,
+                              const char *                       sender,
+                              GDBusMethodInvocation *            invocation,
+                              GVariant *                         parameters)
+{
+    gs_unref_variant GVariant *options = NULL;
+
+    /* NOTE: this handler is called both for NMDevicwWifi and NMDeviceIwd. */
+
+    g_variant_get(parameters, "(@a{sv})", &options);
+
+    _dispatch_request_scan(NM_DEVICE(obj), options, invocation);
+}
+
+const GDBusSignalInfo nm_signal_info_wireless_access_point_added = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
+    "AccessPointAdded",
+    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("access_point", "o"), ), );
+
+const GDBusSignalInfo nm_signal_info_wireless_access_point_removed =
+    NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
+        "AccessPointRemoved",
+        .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("access_point", "o"), ), );
+
+const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_WIRELESS,
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "GetAccessPoints",
+                    .out_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ), ),
+                .handle = impl_device_wifi_get_access_points, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "GetAllAccessPoints",
+                    .out_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ), ),
+                .handle = impl_device_wifi_get_all_access_points, ),
+            NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
+                NM_DEFINE_GDBUS_METHOD_INFO_INIT(
+                    "RequestScan",
+                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                        NM_DEFINE_GDBUS_ARG_INFO("options", "a{sv}"), ), ),
+                .handle = impl_device_wifi_request_scan, ), ),
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy,
+                                                &nm_signal_info_wireless_access_point_added,
+                                                &nm_signal_info_wireless_access_point_removed, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("HwAddress",
+                                                             "s",
+                                                             NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("PermHwAddress",
+                                                             "s",
+                                                             NM_DEVICE_PERM_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Mode", "u", NM_DEVICE_WIFI_MODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Bitrate",
+                                                             "u",
+                                                             NM_DEVICE_WIFI_BITRATE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("AccessPoints",
+                                                             "ao",
+                                                             NM_DEVICE_WIFI_ACCESS_POINTS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ActiveAccessPoint",
+                                                             "o",
+                                                             NM_DEVICE_WIFI_ACTIVE_ACCESS_POINT),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("WirelessCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_WIFI_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("LastScan",
+                                                           "x",
+                                                           NM_DEVICE_WIFI_LAST_SCAN), ), ),
+    .legacy_property_changed = TRUE,
+};
diff --git a/src/core/devices/wifi/nm-wifi-common.h b/src/core/devices/wifi/nm-wifi-common.h
new file mode 100644
index 00000000..fd6f47ed
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-common.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_WIFI_COMMON_H__
+#define __NM_WIFI_COMMON_H__
+
+#include "nm-dbus-utils.h"
+#include "nm-wifi-ap.h"
+
+/*****************************************************************************/
+
+void nm_device_wifi_emit_signal_access_point(NMDevice *device,
+                                             NMWifiAP *ap,
+                                             gboolean  is_added /* or else is_removed */);
+
+extern const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless;
+extern const GDBusSignalInfo             nm_signal_info_wireless_access_point_added;
+extern const GDBusSignalInfo             nm_signal_info_wireless_access_point_removed;
+
+#endif /* __NM_WIFI_COMMON_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-factory.c b/src/core/devices/wifi/nm-wifi-factory.c
new file mode 100644
index 00000000..40375e1c
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-factory.c
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 - 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <gmodule.h>
+
+#include "devices/nm-device-factory.h"
+#include "nm-setting-wireless.h"
+#include "nm-setting-olpc-mesh.h"
+#include "nm-device-wifi.h"
+#include "nm-device-wifi-p2p.h"
+#include "nm-device-olpc-mesh.h"
+#include "nm-device-iwd.h"
+#include "settings/nm-settings-connection.h"
+#include "platform/nm-platform.h"
+#include "nm-config.h"
+
+/*****************************************************************************/
+
+#define NM_TYPE_WIFI_FACTORY (nm_wifi_factory_get_type())
+#define NM_WIFI_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactory))
+#define NM_WIFI_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass))
+#define NM_IS_WIFI_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_FACTORY))
+#define NM_IS_WIFI_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_FACTORY))
+#define NM_WIFI_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_FACTORY, NMWifiFactoryClass))
+
+typedef struct {
+    NMDeviceFactory parent;
+} NMWifiFactory;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMWifiFactoryClass;
+
+static GType nm_wifi_factory_get_type(void);
+
+G_DEFINE_TYPE(NMWifiFactory, nm_wifi_factory, NM_TYPE_DEVICE_FACTORY)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_WIFI, NM_LINK_TYPE_OLPC_MESH)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_WIRELESS_SETTING_NAME,
+                                                NM_SETTING_OLPC_MESH_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_WIFI_FACTORY, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+p2p_device_created(NMDeviceWifi *device, NMDeviceWifiP2P *p2p_device, NMDeviceFactory *self)
+{
+    nm_log_info(LOGD_PLATFORM | LOGD_WIFI,
+                "Wi-Fi P2P device controlled by interface %s created",
+                nm_device_get_iface(NM_DEVICE(device)));
+
+    g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, p2p_device);
+}
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    gs_free char *backend = NULL;
+
+    g_return_val_if_fail(iface != NULL, NULL);
+    g_return_val_if_fail(plink != NULL, NULL);
+    g_return_val_if_fail(g_strcmp0(iface, plink->name) == 0, NULL);
+    g_return_val_if_fail(NM_IN_SET(plink->type, NM_LINK_TYPE_WIFI, NM_LINK_TYPE_OLPC_MESH), NULL);
+
+    if (plink->type != NM_LINK_TYPE_WIFI)
+        return nm_device_olpc_mesh_new(iface);
+
+    backend = nm_config_data_get_device_config_by_pllink(NM_CONFIG_GET_DATA,
+                                                         NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_BACKEND,
+                                                         plink,
+                                                         "wifi",
+                                                         NULL);
+    nm_strstrip(backend);
+
+    nm_log_dbg(LOGD_PLATFORM | LOGD_WIFI,
+               "(%s) config: backend is %s%s%s%s",
+               iface,
+               NM_PRINT_FMT_QUOTE_STRING(backend),
+               WITH_IWD ? " (iwd support enabled)" : "");
+    if (!backend || !g_ascii_strcasecmp(backend, "wpa_supplicant")) {
+        NMDevice *               device;
+        NMDeviceWifiCapabilities capabilities;
+        NM80211Mode              mode;
+
+        if (!nm_platform_wifi_get_capabilities(NM_PLATFORM_GET, plink->ifindex, &capabilities)) {
+            nm_log_warn(LOGD_PLATFORM | LOGD_WIFI,
+                        "(%s) failed to initialize Wi-Fi driver for ifindex %d",
+                        iface,
+                        plink->ifindex);
+            return NULL;
+        }
+
+        /* Ignore monitor-mode and other unhandled interface types.
+         * FIXME: keep TYPE_MONITOR devices in UNAVAILABLE state and manage
+         * them if/when they change to a handled type.
+         */
+        mode = nm_platform_wifi_get_mode(NM_PLATFORM_GET, plink->ifindex);
+        if (mode == NM_802_11_MODE_UNKNOWN) {
+            *out_ignore = TRUE;
+            return NULL;
+        }
+
+        device = nm_device_wifi_new(iface, capabilities);
+
+        g_signal_connect_object(device,
+                                NM_DEVICE_WIFI_P2P_DEVICE_CREATED,
+                                G_CALLBACK(p2p_device_created),
+                                factory,
+                                0);
+
+        return device;
+    }
+#if WITH_IWD
+    else if (!g_ascii_strcasecmp(backend, "iwd"))
+        return nm_device_iwd_new(iface);
+#endif
+
+    nm_log_warn(LOGD_PLATFORM | LOGD_WIFI,
+                "(%s) config: unknown or unsupported wifi-backend %s",
+                iface,
+                backend);
+    return NULL;
+}
+
+/*****************************************************************************/
+
+static void
+nm_wifi_factory_init(NMWifiFactory *self)
+{}
+
+static void
+nm_wifi_factory_class_init(NMWifiFactoryClass *klass)
+{
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    factory_class->create_device       = create_device;
+    factory_class->get_supported_types = get_supported_types;
+}
diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.c b/src/core/devices/wifi/nm-wifi-p2p-peer.c
new file mode 100644
index 00000000..8488f32d
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-p2p-peer.c
@@ -0,0 +1,699 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-wifi-p2p-peer.h"
+
+#include <stdlib.h>
+#include <linux/if_ether.h>
+
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device.h"
+#include "nm-core-internal.h"
+#include "nm-dbus-manager.h"
+#include "nm-glib-aux/nm-ref-string.h"
+#include "nm-setting-wireless.h"
+#include "nm-utils.h"
+#include "nm-wifi-utils.h"
+#include "platform/nm-platform.h"
+#include "supplicant/nm-supplicant-types.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMWifiP2PPeer,
+                             PROP_NAME,
+                             PROP_MANUFACTURER,
+                             PROP_MODEL,
+                             PROP_MODEL_NUMBER,
+                             PROP_SERIAL,
+                             PROP_WFD_IES,
+                             PROP_HW_ADDRESS,
+                             PROP_STRENGTH,
+                             PROP_LAST_SEEN,
+                             PROP_FLAGS, );
+
+struct _NMWifiP2PPeerPrivate {
+    NMRefString *supplicant_path; /* D-Bus object path of this Peer from wpa_supplicant */
+
+    /* Scanned or cached values */
+    char *name;
+    char *manufacturer;
+    char *model;
+    char *model_number;
+    char *serial;
+
+    char *address;
+
+    GBytes *wfd_ies;
+
+    const char **groups;
+
+    guint8 strength;
+
+    NM80211ApFlags flags; /* General flags */
+
+    /* Non-scanned attributes */
+    gint32
+        last_seen; /* Timestamp when the Peer was seen lastly (obtained via nm_utils_get_monotonic_timestamp_sec()) */
+};
+
+typedef struct _NMWifiP2PPeerPrivate NMWifiP2PPeerPrivate;
+
+struct _NMWifiP2PPeerClass {
+    NMDBusObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMWifiP2PPeer, nm_wifi_p2p_peer, NM_TYPE_DBUS_OBJECT)
+
+#define NM_WIFI_P2P_PEER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE_PTR(self, NMWifiP2PPeer, NM_IS_WIFI_P2P_PEER)
+
+/*****************************************************************************/
+
+const char **
+nm_wifi_p2p_peers_get_paths(const CList *peers_lst_head)
+{
+    NMWifiP2PPeer *peer;
+    const char **  list;
+    const char *   path;
+    gsize          i, n;
+
+    n    = c_list_length(peers_lst_head);
+    list = g_new(const char *, n + 1);
+
+    i = 0;
+    if (n > 0) {
+        c_list_for_each_entry (peer, peers_lst_head, peers_lst) {
+            nm_assert(i < n);
+            path = nm_dbus_object_get_path(NM_DBUS_OBJECT(peer));
+            nm_assert(path);
+
+            list[i++] = path;
+        }
+        nm_assert(i <= n);
+    }
+    list[i] = NULL;
+    return list;
+}
+
+NMWifiP2PPeer *
+nm_wifi_p2p_peers_find_first_compatible(const CList *peers_lst_head, NMConnection *connection)
+{
+    NMWifiP2PPeer *peer;
+
+    g_return_val_if_fail(connection, NULL);
+
+    c_list_for_each_entry (peer, peers_lst_head, peers_lst) {
+        if (nm_wifi_p2p_peer_check_compatible(peer, connection))
+            return peer;
+    }
+    return NULL;
+}
+
+NMWifiP2PPeer *
+nm_wifi_p2p_peers_find_by_supplicant_path(const CList *peers_lst_head, const char *path)
+{
+    NMWifiP2PPeer *peer;
+
+    g_return_val_if_fail(path != NULL, NULL);
+
+    c_list_for_each_entry (peer, peers_lst_head, peers_lst) {
+        if (nm_streq0(path, nm_wifi_p2p_peer_get_supplicant_path(peer)))
+            return peer;
+    }
+    return NULL;
+}
+
+/*****************************************************************************/
+
+NMWifiP2PPeer *
+nm_wifi_p2p_peer_lookup_for_device(NMDevice *device, const char *exported_path)
+{
+    NMWifiP2PPeer *peer;
+
+    g_return_val_if_fail(NM_IS_DEVICE(device), NULL);
+
+    peer = (NMWifiP2PPeer *) nm_dbus_manager_lookup_object(
+        nm_dbus_object_get_manager(NM_DBUS_OBJECT(device)),
+        exported_path);
+    if (!peer || !NM_IS_WIFI_P2P_PEER(peer) || peer->wifi_device != device)
+        return NULL;
+
+    return peer;
+}
+
+/*****************************************************************************/
+
+const char *
+nm_wifi_p2p_peer_get_supplicant_path(NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return nm_ref_string_get_str(NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->supplicant_path);
+}
+
+const char *
+nm_wifi_p2p_peer_get_name(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->name;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_name(NMWifiP2PPeer *peer, const char *str)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (!nm_utils_strdup_reset(&priv->name, str))
+        return FALSE;
+    _notify(peer, PROP_NAME);
+    return TRUE;
+}
+
+const char *
+nm_wifi_p2p_peer_get_manufacturer(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->manufacturer;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_manufacturer(NMWifiP2PPeer *peer, const char *str)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (!nm_utils_strdup_reset(&priv->manufacturer, str))
+        return FALSE;
+    _notify(peer, PROP_MANUFACTURER);
+    return TRUE;
+}
+
+const char *
+nm_wifi_p2p_peer_get_model(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->model;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_model(NMWifiP2PPeer *peer, const char *str)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (!nm_utils_strdup_reset(&priv->model, str))
+        return FALSE;
+    _notify(peer, PROP_MODEL);
+    return TRUE;
+}
+
+const char *
+nm_wifi_p2p_peer_get_model_number(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->model_number;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_model_number(NMWifiP2PPeer *peer, const char *str)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (!nm_utils_strdup_reset(&priv->model_number, str))
+        return FALSE;
+    _notify(peer, PROP_MODEL_NUMBER);
+    return TRUE;
+}
+
+const char *
+nm_wifi_p2p_peer_get_serial(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->serial;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_serial(NMWifiP2PPeer *peer, const char *str)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (!nm_utils_strdup_reset(&priv->serial, str))
+        return FALSE;
+    _notify(peer, PROP_SERIAL);
+    return TRUE;
+}
+
+GBytes *
+nm_wifi_p2p_peer_get_wfd_ies(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->wfd_ies;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_wfd_ies(NMWifiP2PPeer *peer, GBytes *wfd_ies)
+{
+    NMWifiP2PPeerPrivate *priv;
+    gs_unref_bytes GBytes *wfd_ies_old = NULL;
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE);
+
+    priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (nm_gbytes_equal0(priv->wfd_ies, wfd_ies))
+        return FALSE;
+
+    wfd_ies_old   = g_steal_pointer(&priv->wfd_ies);
+    priv->wfd_ies = wfd_ies ? g_bytes_ref(wfd_ies) : NULL;
+
+    _notify(peer, PROP_WFD_IES);
+    return TRUE;
+}
+
+const char *const *
+nm_wifi_p2p_peer_get_groups(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->groups;
+}
+
+const char *
+nm_wifi_p2p_peer_get_address(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NULL);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->address;
+}
+
+static gboolean
+nm_wifi_p2p_peer_set_address_bin(NMWifiP2PPeer *peer, const guint8 addr[static ETH_ALEN])
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (priv->address && nm_utils_hwaddr_matches(addr, ETH_ALEN, priv->address, -1))
+        return FALSE;
+
+    g_free(priv->address);
+    priv->address = nm_utils_hwaddr_ntoa(addr, ETH_ALEN);
+    _notify(peer, PROP_HW_ADDRESS);
+    return TRUE;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_address(NMWifiP2PPeer *peer, const char *addr)
+{
+    guint8 addr_buf[ETH_ALEN];
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE);
+
+    if (!addr || !nm_utils_hwaddr_aton(addr, addr_buf, sizeof(addr_buf)))
+        g_return_val_if_reached(FALSE);
+
+    return nm_wifi_p2p_peer_set_address_bin(peer, addr_buf);
+}
+
+gint8
+nm_wifi_p2p_peer_get_strength(NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), 0);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->strength;
+}
+
+gboolean
+nm_wifi_p2p_peer_set_strength(NMWifiP2PPeer *peer, const gint8 strength)
+{
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (priv->strength != strength) {
+        priv->strength = strength;
+        _notify(peer, PROP_STRENGTH);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+NM80211ApFlags
+nm_wifi_p2p_peer_get_flags(const NMWifiP2PPeer *peer)
+{
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), NM_802_11_AP_FLAGS_NONE);
+
+    return NM_WIFI_P2P_PEER_GET_PRIVATE(peer)->flags;
+}
+
+static gboolean
+nm_wifi_p2p_peer_set_last_seen(NMWifiP2PPeer *peer, gint32 last_seen)
+{
+    NMWifiP2PPeerPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE);
+
+    priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    if (priv->last_seen != last_seen) {
+        priv->last_seen = last_seen;
+        _notify(peer, PROP_LAST_SEEN);
+        return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_wifi_p2p_peer_update_from_properties(NMWifiP2PPeer *peer, const NMSupplicantPeerInfo *peer_info)
+{
+    NMWifiP2PPeerPrivate *priv;
+    gboolean              changed = FALSE;
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(peer), FALSE);
+    g_return_val_if_fail(peer_info, FALSE);
+    nm_assert(NM_IS_REF_STRING(peer_info->peer_path));
+
+    priv = NM_WIFI_P2P_PEER_GET_PRIVATE(peer);
+
+    nm_assert(!priv->supplicant_path || priv->supplicant_path == peer_info->peer_path);
+
+    g_object_freeze_notify(G_OBJECT(peer));
+
+    if (!priv->supplicant_path) {
+        priv->supplicant_path = nm_ref_string_ref(peer_info->peer_path);
+        changed               = TRUE;
+    }
+
+    changed |= nm_wifi_p2p_peer_set_strength(peer, peer_info->signal_percent);
+    changed |= nm_wifi_p2p_peer_set_name(peer, peer_info->device_name);
+    changed |= nm_wifi_p2p_peer_set_manufacturer(peer, peer_info->manufacturer);
+    changed |= nm_wifi_p2p_peer_set_model(peer, peer_info->model);
+    changed |= nm_wifi_p2p_peer_set_model_number(peer, peer_info->model_number);
+    changed |= nm_wifi_p2p_peer_set_serial(peer, peer_info->serial);
+
+    if (peer_info->address_valid)
+        changed |= nm_wifi_p2p_peer_set_address_bin(peer, peer_info->address);
+    else {
+        /* we don't reset the address. */
+    }
+
+    changed |= nm_wifi_p2p_peer_set_wfd_ies(peer, peer_info->ies);
+    changed |= nm_wifi_p2p_peer_set_last_seen(peer, peer_info->last_seen_msec / 1000u);
+
+    /* We currently only use the groups information internally to check if
+     * the peer is still joined. */
+    if (!nm_utils_strv_equal(priv->groups, peer_info->groups)) {
+        g_free(priv->groups);
+        priv->groups = nm_utils_strv_dup_packed(peer_info->groups, -1);
+        changed |= TRUE;
+    }
+
+    g_object_thaw_notify(G_OBJECT(peer));
+
+    return changed;
+}
+
+const char *
+nm_wifi_p2p_peer_to_string(const NMWifiP2PPeer *self, char *str_buf, gsize buf_len, gint32 now_s)
+{
+    const NMWifiP2PPeerPrivate *priv;
+    const char *                supplicant_id = "-";
+    const char *                export_path;
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(self), NULL);
+
+    priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self);
+
+    if (priv->supplicant_path)
+        supplicant_id = strrchr(priv->supplicant_path->str, '/') ?: supplicant_id;
+
+    export_path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self));
+    if (export_path)
+        export_path = strrchr(export_path, '/') ?: export_path;
+    else
+        export_path = "/";
+
+    g_snprintf(str_buf,
+               buf_len,
+               "%17s [n:%s, m:%s, mod:%s, mod_num:%s, ser:%s] %3us sup:%s [nm:%s]",
+               priv->address ?: "(none)",
+               priv->name,
+               priv->manufacturer,
+               priv->model,
+               priv->model_number,
+               priv->serial,
+               priv->last_seen > 0 ? ((now_s > 0 ? now_s : nm_utils_get_monotonic_timestamp_sec())
+                                      - priv->last_seen)
+                                   : -1,
+               supplicant_id,
+               export_path);
+
+    return str_buf;
+}
+
+gboolean
+nm_wifi_p2p_peer_check_compatible(NMWifiP2PPeer *self, NMConnection *connection)
+{
+    NMWifiP2PPeerPrivate *priv;
+    NMSettingWifiP2P *    s_wifi_p2p;
+    const char *          hwaddr;
+
+    g_return_val_if_fail(NM_IS_WIFI_P2P_PEER(self), FALSE);
+    g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
+
+    priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self);
+
+    s_wifi_p2p =
+        NM_SETTING_WIFI_P2P(nm_connection_get_setting(connection, NM_TYPE_SETTING_WIFI_P2P));
+    if (s_wifi_p2p == NULL)
+        return FALSE;
+
+    hwaddr = nm_setting_wifi_p2p_get_peer(s_wifi_p2p);
+    if (hwaddr && (!priv->address || !nm_utils_hwaddr_matches(hwaddr, -1, priv->address, -1)))
+        return FALSE;
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMWifiP2PPeer *       self = NM_WIFI_P2P_PEER(object);
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_FLAGS:
+        g_value_set_uint(value, priv->flags);
+        break;
+    case PROP_NAME:
+        g_value_set_string(value, priv->name);
+        break;
+    case PROP_MANUFACTURER:
+        g_value_set_string(value, priv->manufacturer);
+        break;
+    case PROP_MODEL:
+        g_value_set_string(value, priv->model);
+        break;
+    case PROP_MODEL_NUMBER:
+        g_value_set_string(value, priv->model_number);
+        break;
+    case PROP_SERIAL:
+        g_value_set_string(value, priv->serial);
+        break;
+    case PROP_WFD_IES:
+        g_value_take_variant(value, nm_utils_gbytes_to_variant_ay(priv->wfd_ies));
+        break;
+    case PROP_HW_ADDRESS:
+        g_value_set_string(value, priv->address);
+        break;
+    case PROP_STRENGTH:
+        g_value_set_uchar(value, priv->strength);
+        break;
+    case PROP_LAST_SEEN:
+        g_value_set_int(value,
+                        priv->last_seen > 0
+                            ? (int) nm_utils_monotonic_timestamp_as_boottime(priv->last_seen,
+                                                                             NM_UTILS_NSEC_PER_SEC)
+                            : -1);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_wifi_p2p_peer_init(NMWifiP2PPeer *self)
+{
+    NMWifiP2PPeerPrivate *priv;
+
+    priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerPrivate);
+
+    self->_priv = priv;
+
+    c_list_init(&self->peers_lst);
+
+    priv->flags     = NM_802_11_AP_FLAGS_NONE;
+    priv->last_seen = -1;
+}
+
+NMWifiP2PPeer *
+nm_wifi_p2p_peer_new_from_properties(const NMSupplicantPeerInfo *peer_info)
+{
+    NMWifiP2PPeer *peer;
+
+    g_return_val_if_fail(peer_info, NULL);
+
+    peer = g_object_new(NM_TYPE_WIFI_P2P_PEER, NULL);
+    nm_wifi_p2p_peer_update_from_properties(peer, peer_info);
+    return peer;
+}
+
+static void
+finalize(GObject *object)
+{
+    NMWifiP2PPeer *       self = NM_WIFI_P2P_PEER(object);
+    NMWifiP2PPeerPrivate *priv = NM_WIFI_P2P_PEER_GET_PRIVATE(self);
+
+    nm_assert(!self->wifi_device);
+    nm_assert(c_list_is_empty(&self->peers_lst));
+
+    nm_ref_string_unref(priv->supplicant_path);
+    g_free(priv->name);
+    g_free(priv->manufacturer);
+    g_free(priv->model);
+    g_free(priv->model_number);
+    g_free(priv->serial);
+    g_free(priv->address);
+    g_bytes_unref(priv->wfd_ies);
+    g_free(priv->groups);
+
+    G_OBJECT_CLASS(nm_wifi_p2p_peer_parent_class)->finalize(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_p2p_peer = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_WIFI_P2P_PEER,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            /* Before 1.24, we wrongly exposed a property "Groups" of type "as". Don't reuse that property name. */
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Flags", "u", NM_WIFI_P2P_PEER_FLAGS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Name", "s", NM_WIFI_P2P_PEER_NAME),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Manufacturer",
+                                                           "s",
+                                                           NM_WIFI_P2P_PEER_MANUFACTURER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Model", "s", NM_WIFI_P2P_PEER_MODEL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("ModelNumber",
+                                                           "s",
+                                                           NM_WIFI_P2P_PEER_MODEL_NUMBER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Serial", "s", NM_WIFI_P2P_PEER_SERIAL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WfdIEs",
+                                                           "ay",
+                                                           NM_WIFI_P2P_PEER_WFD_IES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
+                                                           "s",
+                                                           NM_WIFI_P2P_PEER_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Strength",
+                                                           "y",
+                                                           NM_WIFI_P2P_PEER_STRENGTH),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("LastSeen",
+                                                           "i",
+                                                           NM_WIFI_P2P_PEER_LAST_SEEN), ), ),
+    .legacy_property_changed = FALSE,
+};
+
+static void
+nm_wifi_p2p_peer_class_init(NMWifiP2PPeerClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMWifiP2PPeerPrivate));
+
+    dbus_object_class->export_path     = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH_WIFI_P2P_PEER);
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_p2p_peer);
+
+    object_class->get_property = get_property;
+    object_class->finalize     = finalize;
+
+    obj_properties[PROP_FLAGS] = g_param_spec_uint(NM_WIFI_P2P_PEER_FLAGS,
+                                                   "",
+                                                   "",
+                                                   NM_802_11_AP_FLAGS_NONE,
+                                                   NM_802_11_AP_FLAGS_PRIVACY,
+                                                   NM_802_11_AP_FLAGS_NONE,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_NAME] = g_param_spec_string(NM_WIFI_P2P_PEER_NAME,
+                                                    "",
+                                                    "",
+                                                    NULL,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MANUFACTURER] =
+        g_param_spec_string(NM_WIFI_P2P_PEER_MANUFACTURER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MODEL] = g_param_spec_string(NM_WIFI_P2P_PEER_MODEL,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MODEL_NUMBER] =
+        g_param_spec_string(NM_WIFI_P2P_PEER_MODEL_NUMBER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SERIAL] = g_param_spec_string(NM_WIFI_P2P_PEER_SERIAL,
+                                                      "",
+                                                      "",
+                                                      NULL,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_WFD_IES] = g_param_spec_variant(NM_WIFI_P2P_PEER_WFD_IES,
+                                                        "",
+                                                        "",
+                                                        G_VARIANT_TYPE("ay"),
+                                                        NULL,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_HW_ADDRESS] =
+        g_param_spec_string(NM_WIFI_P2P_PEER_HW_ADDRESS,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_STRENGTH] = g_param_spec_uchar(NM_WIFI_P2P_PEER_STRENGTH,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       G_MAXINT8,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_LAST_SEEN] = g_param_spec_int(NM_WIFI_P2P_PEER_LAST_SEEN,
+                                                      "",
+                                                      "",
+                                                      -1,
+                                                      G_MAXINT,
+                                                      -1,
+                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.h b/src/core/devices/wifi/nm-wifi-p2p-peer.h
new file mode 100644
index 00000000..ee4bfb53
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-p2p-peer.h
@@ -0,0 +1,91 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#ifndef __NM_WIFI_P2P_PEER_H__
+#define __NM_WIFI_P2P_PEER_H__
+
+#include "nm-dbus-object.h"
+#include "nm-dbus-interface.h"
+#include "nm-connection.h"
+
+#define NM_TYPE_WIFI_P2P_PEER (nm_wifi_p2p_peer_get_type())
+#define NM_WIFI_P2P_PEER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeer))
+#define NM_WIFI_P2P_PEER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass))
+#define NM_IS_WIFI_P2P_PEER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WIFI_P2P_PEER))
+#define NM_IS_WIFI_P2P_PEER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WIFI_P2P_PEER))
+#define NM_WIFI_P2P_PEER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WIFI_P2P_PEER, NMWifiP2PPeerClass))
+
+#define NM_WIFI_P2P_PEER_FLAGS        "flags"
+#define NM_WIFI_P2P_PEER_NAME         "name"
+#define NM_WIFI_P2P_PEER_MANUFACTURER "manufacturer"
+#define NM_WIFI_P2P_PEER_MODEL        "model"
+#define NM_WIFI_P2P_PEER_MODEL_NUMBER "model-number"
+#define NM_WIFI_P2P_PEER_SERIAL       "serial"
+#define NM_WIFI_P2P_PEER_WFD_IES      "wfd-ies"
+#define NM_WIFI_P2P_PEER_HW_ADDRESS   "hw-address"
+#define NM_WIFI_P2P_PEER_STRENGTH     "strength"
+#define NM_WIFI_P2P_PEER_LAST_SEEN    "last-seen"
+
+typedef struct {
+    NMDBusObject                  parent;
+    NMDevice *                    wifi_device;
+    CList                         peers_lst;
+    struct _NMWifiP2PPeerPrivate *_priv;
+} NMWifiP2PPeer;
+
+typedef struct _NMWifiP2PPeerClass NMWifiP2PPeerClass;
+
+struct _NMSupplicantPeerInfo;
+
+GType nm_wifi_p2p_peer_get_type(void);
+
+NMWifiP2PPeer *nm_wifi_p2p_peer_new_from_properties(const struct _NMSupplicantPeerInfo *peer_info);
+
+gboolean nm_wifi_p2p_peer_update_from_properties(NMWifiP2PPeer *                     peer,
+                                                 const struct _NMSupplicantPeerInfo *peer_info);
+
+gboolean nm_wifi_p2p_peer_check_compatible(NMWifiP2PPeer *self, NMConnection *connection);
+
+const char *nm_wifi_p2p_peer_get_supplicant_path(NMWifiP2PPeer *peer);
+
+const char *nm_wifi_p2p_peer_get_name(const NMWifiP2PPeer *peer);
+gboolean    nm_wifi_p2p_peer_set_name(NMWifiP2PPeer *peer, const char *name);
+const char *nm_wifi_p2p_peer_get_manufacturer(const NMWifiP2PPeer *peer);
+gboolean    nm_wifi_p2p_peer_set_manufacturer(NMWifiP2PPeer *peer, const char *manufacturer);
+const char *nm_wifi_p2p_peer_get_model(const NMWifiP2PPeer *peer);
+gboolean    nm_wifi_p2p_peer_set_model(NMWifiP2PPeer *peer, const char *model);
+const char *nm_wifi_p2p_peer_get_model_number(const NMWifiP2PPeer *peer);
+gboolean    nm_wifi_p2p_peer_set_model_number(NMWifiP2PPeer *peer, const char *number);
+const char *nm_wifi_p2p_peer_get_serial(const NMWifiP2PPeer *peer);
+gboolean    nm_wifi_p2p_peer_set_serial(NMWifiP2PPeer *peer, const char *serial);
+
+GBytes * nm_wifi_p2p_peer_get_wfd_ies(const NMWifiP2PPeer *peer);
+gboolean nm_wifi_p2p_peer_set_wfd_ies(NMWifiP2PPeer *peer, GBytes *bytes);
+
+const char *const *nm_wifi_p2p_peer_get_groups(const NMWifiP2PPeer *peer);
+
+const char *   nm_wifi_p2p_peer_get_address(const NMWifiP2PPeer *peer);
+gboolean       nm_wifi_p2p_peer_set_address(NMWifiP2PPeer *peer, const char *addr);
+gint8          nm_wifi_p2p_peer_get_strength(NMWifiP2PPeer *peer);
+gboolean       nm_wifi_p2p_peer_set_strength(NMWifiP2PPeer *peer, gint8 strength);
+NM80211ApFlags nm_wifi_p2p_peer_get_flags(const NMWifiP2PPeer *self);
+
+const char *
+nm_wifi_p2p_peer_to_string(const NMWifiP2PPeer *self, char *str_buf, gsize buf_len, gint32 now_s);
+
+const char **nm_wifi_p2p_peers_get_paths(const CList *peers_lst_head);
+
+NMWifiP2PPeer *nm_wifi_p2p_peers_find_first_compatible(const CList * peers_lst_head,
+                                                       NMConnection *connection);
+
+NMWifiP2PPeer *nm_wifi_p2p_peers_find_by_supplicant_path(const CList *peers_lst_head,
+                                                         const char * path);
+
+NMWifiP2PPeer *nm_wifi_p2p_peer_lookup_for_device(NMDevice *device, const char *exported_path);
+
+#endif /* __NM_WIFI_P2P_PEER_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c
new file mode 100644
index 00000000..aed236cc
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-utils.c
@@ -0,0 +1,945 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-wifi-utils.h"
+
+#include <stdlib.h>
+
+#include "nm-utils.h"
+#include "nm-core-internal.h"
+
+static gboolean
+verify_no_wep(NMSettingWirelessSecurity *s_wsec, const char *tag, GError **error)
+{
+    if (nm_setting_wireless_security_get_wep_key(s_wsec, 0)
+        || nm_setting_wireless_security_get_wep_key(s_wsec, 1)
+        || nm_setting_wireless_security_get_wep_key(s_wsec, 2)
+        || nm_setting_wireless_security_get_wep_key(s_wsec, 3)
+        || nm_setting_wireless_security_get_wep_tx_keyidx(s_wsec)
+        || nm_setting_wireless_security_get_wep_key_type(s_wsec)) {
+        /* Dynamic WEP cannot have any WEP keys set */
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                    _("%s is incompatible with static WEP keys"),
+                    tag);
+        g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_leap(NMSettingWirelessSecurity *s_wsec,
+            NMSetting8021x *           s_8021x,
+            gboolean                   adhoc,
+            GError **                  error)
+{
+    const char *key_mgmt, *auth_alg, *leap_username;
+
+    key_mgmt      = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    auth_alg      = nm_setting_wireless_security_get_auth_alg(s_wsec);
+    leap_username = nm_setting_wireless_security_get_leap_username(s_wsec);
+
+    /* One (or both) of two things indicates we want LEAP:
+     * 1) auth_alg == 'leap'
+     * 2) valid leap_username
+     *
+     * LEAP always requires a LEAP username.
+     */
+
+    if (auth_alg) {
+        if (!strcmp(auth_alg, "leap")) {
+            /* LEAP authentication requires at least a LEAP username */
+            if (!leap_username) {
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_MISSING_PROPERTY,
+                                    _("LEAP authentication requires a LEAP username"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                               NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME);
+                return FALSE;
+            }
+        } else if (leap_username) {
+            /* Leap username requires 'leap' auth */
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("LEAP username requires 'leap' authentication"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME);
+            return FALSE;
+        }
+    }
+
+    if (leap_username) {
+        if (key_mgmt && strcmp(key_mgmt, "ieee8021x")) {
+            /* LEAP requires ieee8021x key management */
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("LEAP authentication requires IEEE 802.1x key management"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+            return FALSE;
+        }
+    }
+
+    /* At this point if auth_alg is set it must be 'leap', and if key_mgmt
+     * is set it must be 'ieee8021x'.
+     */
+    if (leap_username) {
+        if (auth_alg)
+            g_assert(strcmp(auth_alg, "leap") == 0);
+        if (key_mgmt)
+            g_assert(strcmp(key_mgmt, "ieee8021x") == 0);
+
+        if (adhoc) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("LEAP authentication is incompatible with Ad-Hoc mode"));
+            g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME);
+            return FALSE;
+        }
+
+        if (!verify_no_wep(s_wsec, "LEAP", error))
+            return FALSE;
+
+        if (s_8021x) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("LEAP authentication is incompatible with 802.1x setting"));
+            g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_no_wpa(NMSettingWirelessSecurity *s_wsec, const char *tag, GError **error)
+{
+    const char *key_mgmt;
+    int         n, i;
+
+    key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    if (key_mgmt && !strncmp(key_mgmt, "wpa", 3)) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("a connection using '%s' authentication cannot use WPA key management"),
+                    tag);
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+        return FALSE;
+    }
+
+    if (nm_setting_wireless_security_get_num_protos(s_wsec)) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("a connection using '%s' authentication cannot specify WPA protocols"),
+                    tag);
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_PROTO);
+        return FALSE;
+    }
+
+    n = nm_setting_wireless_security_get_num_pairwise(s_wsec);
+    for (i = 0; i < n; i++) {
+        const char *pw;
+
+        pw = nm_setting_wireless_security_get_pairwise(s_wsec, i);
+        if (!strcmp(pw, "tkip") || !strcmp(pw, "ccmp")) {
+            g_set_error(error,
+                        NM_CONNECTION_ERROR,
+                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                        _("a connection using '%s' authentication cannot specify WPA ciphers"),
+                        tag);
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_PAIRWISE);
+            return FALSE;
+        }
+    }
+
+    n = nm_setting_wireless_security_get_num_groups(s_wsec);
+    for (i = 0; i < n; i++) {
+        const char *gr;
+
+        gr = nm_setting_wireless_security_get_group(s_wsec, i);
+        if (strcmp(gr, "wep40") && strcmp(gr, "wep104")) {
+            g_set_error(error,
+                        NM_CONNECTION_ERROR,
+                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                        _("a connection using '%s' authentication cannot specify WPA ciphers"),
+                        tag);
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_GROUP);
+            return FALSE;
+        }
+    }
+
+    if (nm_setting_wireless_security_get_psk(s_wsec)) {
+        g_set_error(error,
+                    NM_CONNECTION_ERROR,
+                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                    _("a connection using '%s' authentication cannot specify a WPA password"),
+                    tag);
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_PSK);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_dynamic_wep(NMSettingWirelessSecurity *s_wsec,
+                   NMSetting8021x *           s_8021x,
+                   gboolean                   adhoc,
+                   GError **                  error)
+{
+    const char *key_mgmt, *auth_alg, *leap_username;
+
+    key_mgmt      = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    auth_alg      = nm_setting_wireless_security_get_auth_alg(s_wsec);
+    leap_username = nm_setting_wireless_security_get_leap_username(s_wsec);
+
+    g_return_val_if_fail(leap_username == NULL, TRUE);
+
+    if (key_mgmt) {
+        if (!strcmp(key_mgmt, "ieee8021x")) {
+            if (!s_8021x) {
+                /* 802.1x key management requires an 802.1x setting */
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_MISSING_SETTING,
+                                    _("Dynamic WEP requires an 802.1x setting"));
+                g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+                return FALSE;
+            }
+
+            if (auth_alg && strcmp(auth_alg, "open")) {
+                /* 802.1x key management must use "open" authentication */
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    _("Dynamic WEP requires 'open' authentication"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                               NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+                return FALSE;
+            }
+
+            /* Dynamic WEP incompatible with anything static WEP related */
+            if (!verify_no_wep(s_wsec, "Dynamic WEP", error))
+                return FALSE;
+        } else if (!strcmp(key_mgmt, "none")) {
+            if (s_8021x) {
+                /* 802.1x setting requires 802.1x key management */
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    _("Dynamic WEP requires 'ieee8021x' key management"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                               NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+                return FALSE;
+            }
+        }
+    } else if (s_8021x) {
+        /* 802.1x setting incompatible with anything but 'open' auth */
+        if (auth_alg && strcmp(auth_alg, "open")) {
+            /* 802.1x key management must use "open" authentication */
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("Dynamic WEP requires 'open' authentication"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+            return FALSE;
+        }
+
+        /* Dynamic WEP incompatible with anything static WEP related */
+        if (!verify_no_wep(s_wsec, "Dynamic WEP", error))
+            return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_wpa_psk(NMSettingWirelessSecurity *s_wsec,
+               NMSetting8021x *           s_8021x,
+               gboolean                   adhoc,
+               guint32                    wpa_flags,
+               guint32                    rsn_flags,
+               GError **                  error)
+{
+    const char *key_mgmt, *auth_alg;
+
+    key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec);
+
+    if (!nm_streq0(key_mgmt, "wpa-psk"))
+        return TRUE;
+
+    if (s_8021x) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_SETTING,
+                            _("WPA-PSK authentication is incompatible with 802.1x"));
+        g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+        return FALSE;
+    }
+
+    if (auth_alg && !nm_streq(auth_alg, "open")) {
+        /* WPA must use "open" authentication */
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("WPA-PSK requires 'open' authentication"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+        return FALSE;
+    }
+
+    /* Make sure the AP's capabilities support WPA-PSK */
+    if (!(wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)
+        && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Access point does not support PSK but setting requires it"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+        return FALSE;
+    }
+
+    if (adhoc) {
+        /* Ad-Hoc RSN requires 'rsn' proto, 'ccmp' pairwise, and 'ccmp' group */
+        if (nm_setting_wireless_security_get_num_protos(s_wsec) != 1
+            || !nm_streq0(nm_setting_wireless_security_get_proto(s_wsec, 0), "rsn")) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("WPA Ad-Hoc authentication requires 'rsn' protocol"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_PROTO);
+            return FALSE;
+        }
+
+        if (nm_setting_wireless_security_get_num_pairwise(s_wsec) != 1
+            || !nm_streq0(nm_setting_wireless_security_get_pairwise(s_wsec, 0), "ccmp")) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("WPA Ad-Hoc authentication requires 'ccmp' pairwise cipher"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_PAIRWISE);
+            return FALSE;
+        }
+
+        if (nm_setting_wireless_security_get_num_groups(s_wsec) != 1
+            || !nm_streq0(nm_setting_wireless_security_get_group(s_wsec, 0), "ccmp")) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                _("WPA Ad-Hoc requires 'ccmp' group cipher"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                           NM_SETTING_WIRELESS_SECURITY_GROUP);
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_wpa_eap(NMSettingWirelessSecurity *s_wsec,
+               NMSetting8021x *           s_8021x,
+               guint32                    wpa_flags,
+               guint32                    rsn_flags,
+               GError **                  error)
+{
+    const char *key_mgmt, *auth_alg;
+    gboolean    is_wpa_eap = FALSE;
+
+    key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    auth_alg = nm_setting_wireless_security_get_auth_alg(s_wsec);
+
+    if (key_mgmt) {
+        if (NM_IN_STRSET(key_mgmt, "wpa-eap", "wpa-eap-suite-b-192")) {
+            if (!s_8021x) {
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_MISSING_SETTING,
+                                    _("WPA-EAP authentication requires an 802.1x setting"));
+                g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+                return FALSE;
+            }
+
+            if (auth_alg && strcmp(auth_alg, "open")) {
+                /* WPA must use "open" authentication */
+                g_set_error_literal(error,
+                                    NM_CONNECTION_ERROR,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    _("WPA-EAP requires 'open' authentication"));
+                g_prefix_error(error,
+                               "%s.%s: ",
+                               NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                               NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+                return FALSE;
+            }
+
+            is_wpa_eap = TRUE;
+        } else if (s_8021x) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("802.1x setting requires 'wpa-eap' key management"));
+            g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+            return FALSE;
+        }
+    }
+
+    if (is_wpa_eap || s_8021x) {
+        /* Make sure the AP's capabilities support WPA-EAP */
+        if (!(wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)
+            && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)
+            && !(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("Access point does not support 802.1x but setting requires it"));
+            g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+verify_adhoc(NMSettingWirelessSecurity *s_wsec,
+             NMSetting8021x *           s_8021x,
+             gboolean                   adhoc,
+             GError **                  error)
+{
+    const char *key_mgmt = NULL, *leap_username = NULL, *auth_alg = NULL;
+
+    if (!adhoc)
+        return TRUE;
+
+    if (s_wsec) {
+        key_mgmt      = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+        auth_alg      = nm_setting_wireless_security_get_auth_alg(s_wsec);
+        leap_username = nm_setting_wireless_security_get_leap_username(s_wsec);
+    }
+
+    if (key_mgmt && !NM_IN_STRSET(key_mgmt, "none", "wpa-psk")) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Ad-Hoc mode requires 'none' or 'wpa-psk' key management"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+        return FALSE;
+    }
+
+    if (s_8021x) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_SETTING,
+                            _("Ad-Hoc mode is incompatible with 802.1x security"));
+        g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+        return FALSE;
+    }
+
+    if (leap_username) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Ad-Hoc mode is incompatible with LEAP security"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+        return FALSE;
+    }
+
+    if (auth_alg && !nm_streq(auth_alg, "open")) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("Ad-Hoc mode requires 'open' authentication"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+nm_wifi_utils_complete_connection(GBytes *      ap_ssid,
+                                  const char *  bssid,
+                                  NM80211Mode   ap_mode,
+                                  guint32       ap_freq,
+                                  guint32       ap_flags,
+                                  guint32       ap_wpa_flags,
+                                  guint32       ap_rsn_flags,
+                                  NMConnection *connection,
+                                  gboolean      lock_bssid,
+                                  GError **     error)
+{
+    NMSettingWireless *        s_wifi;
+    NMSettingWirelessSecurity *s_wsec;
+    NMSetting8021x *           s_8021x;
+    GBytes *                   ssid;
+    const char *               mode, *key_mgmt, *auth_alg, *leap_username;
+    gboolean                   adhoc = FALSE;
+    gboolean                   mesh  = FALSE;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    g_assert(s_wifi);
+    s_wsec  = nm_connection_get_setting_wireless_security(connection);
+    s_8021x = nm_connection_get_setting_802_1x(connection);
+
+    /* Fill in missing SSID */
+    ssid = nm_setting_wireless_get_ssid(s_wifi);
+    if (!ssid)
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_SSID, ap_ssid, NULL);
+    else if (!ap_ssid || !g_bytes_equal(ssid, ap_ssid)) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("connection does not match access point"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SSID);
+        return FALSE;
+    }
+
+    if (lock_bssid && !nm_setting_wireless_get_bssid(s_wifi))
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_BSSID, bssid, NULL);
+
+    /* And mode */
+    mode = nm_setting_wireless_get_mode(s_wifi);
+    if (mode) {
+        gboolean valid = FALSE;
+
+        /* Make sure the supplied mode matches the AP's */
+        if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_INFRA)
+            || !strcmp(mode, NM_SETTING_WIRELESS_MODE_AP)) {
+            if (ap_mode == NM_802_11_MODE_INFRA)
+                valid = TRUE;
+        } else if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_ADHOC)) {
+            if (ap_mode == NM_802_11_MODE_ADHOC)
+                valid = TRUE;
+            adhoc = TRUE;
+        } else if (!strcmp(mode, NM_SETTING_WIRELESS_MODE_MESH)) {
+            if (ap_mode == NM_802_11_MODE_MESH)
+                valid = TRUE;
+            mesh = TRUE;
+        }
+
+        if (valid == FALSE) {
+            g_set_error(error,
+                        NM_CONNECTION_ERROR,
+                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                        _("connection does not match access point"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SETTING_NAME,
+                           NM_SETTING_WIRELESS_MODE);
+            return FALSE;
+        }
+    } else {
+        mode = NM_SETTING_WIRELESS_MODE_INFRA;
+        if (ap_mode == NM_802_11_MODE_ADHOC) {
+            mode  = NM_SETTING_WIRELESS_MODE_ADHOC;
+            adhoc = TRUE;
+        } else if (ap_mode == NM_802_11_MODE_MESH) {
+            mode = NM_SETTING_WIRELESS_MODE_MESH;
+            mesh = TRUE;
+        }
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, mode, NULL);
+    }
+
+    /* For now mesh requires channel and band, fill them only if both not present.
+     * Do not check existing values against an existing ap/mesh point,
+     * mesh join will start a new network if required */
+    if (mesh) {
+        const char *band;
+        guint32     channel;
+        gboolean    band_valid = TRUE;
+        gboolean    chan_valid = TRUE;
+        gboolean    valid;
+
+        band    = nm_setting_wireless_get_band(s_wifi);
+        channel = nm_setting_wireless_get_channel(s_wifi);
+
+        valid = ((band == NULL) && (channel == 0)) || ((band != NULL) && (channel != 0));
+
+        if ((band == NULL) && (channel == 0)) {
+            channel = nm_utils_wifi_freq_to_channel(ap_freq);
+            if (channel) {
+                g_object_set(s_wifi, NM_SETTING_WIRELESS_CHANNEL, channel, NULL);
+            } else {
+                chan_valid = FALSE;
+            }
+
+            band = nm_utils_wifi_freq_to_band(ap_freq);
+            if (band) {
+                g_object_set(s_wifi, NM_SETTING_WIRELESS_BAND, band, NULL);
+            } else {
+                band_valid = FALSE;
+            }
+        }
+
+        if (!valid || !chan_valid || !band_valid) {
+            g_set_error(error,
+                        NM_CONNECTION_ERROR,
+                        NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                        _("connection does not match mesh point"));
+            g_prefix_error(error,
+                           "%s.%s: ",
+                           NM_SETTING_WIRELESS_SETTING_NAME,
+                           NM_SETTING_WIRELESS_MODE);
+            return FALSE;
+        }
+    }
+
+    /* Security */
+
+    /* Open */
+    if (!(ap_flags & NM_802_11_AP_FLAGS_PRIVACY) && (ap_wpa_flags == NM_802_11_AP_SEC_NONE)
+        && (ap_rsn_flags == NM_802_11_AP_SEC_NONE)) {
+        /* Make sure the connection doesn't specify security */
+        if (s_wsec || s_8021x) {
+            g_set_error_literal(error,
+                                NM_CONNECTION_ERROR,
+                                NM_CONNECTION_ERROR_INVALID_SETTING,
+                                _("Access point is unencrypted but setting specifies security"));
+            if (s_wsec)
+                g_prefix_error(error, "%s: ", NM_SETTING_WIRELESS_SECURITY_SETTING_NAME);
+            else
+                g_prefix_error(error, "%s: ", NM_SETTING_802_1X_SETTING_NAME);
+            return FALSE;
+        }
+        return TRUE;
+    }
+
+    /* Everything else requires security */
+    if (!s_wsec) {
+        s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wsec));
+    }
+
+    key_mgmt      = nm_setting_wireless_security_get_key_mgmt(s_wsec);
+    auth_alg      = nm_setting_wireless_security_get_auth_alg(s_wsec);
+    leap_username = nm_setting_wireless_security_get_leap_username(s_wsec);
+
+    /* Ad-Hoc checks */
+    if (!verify_adhoc(s_wsec, s_8021x, adhoc, error))
+        return FALSE;
+
+    /* Static WEP, Dynamic WEP, or LEAP */
+    if ((ap_flags & NM_802_11_AP_FLAGS_PRIVACY) && (ap_wpa_flags == NM_802_11_AP_SEC_NONE)
+        && (ap_rsn_flags == NM_802_11_AP_SEC_NONE)) {
+        const char *tag            = "WEP";
+        gboolean    is_dynamic_wep = FALSE;
+
+        if (!verify_leap(s_wsec, s_8021x, adhoc, error))
+            return FALSE;
+
+        if (leap_username) {
+            tag = "LEAP";
+        } else {
+            /* Static or Dynamic WEP */
+            if (!verify_dynamic_wep(s_wsec, s_8021x, adhoc, error))
+                return FALSE;
+
+            if (s_8021x || (key_mgmt && !strcmp(key_mgmt, "ieee8021x"))) {
+                is_dynamic_wep = TRUE;
+                tag            = "Dynamic WEP";
+            }
+        }
+
+        /* Nothing WPA-related can be set */
+        if (!verify_no_wpa(s_wsec, tag, error))
+            return FALSE;
+
+        if (leap_username) {
+            /* LEAP */
+            g_object_set(s_wsec,
+                         NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                         "ieee8021x",
+                         NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                         "leap",
+                         NULL);
+        } else if (is_dynamic_wep) {
+            /* Dynamic WEP */
+            g_object_set(s_wsec,
+                         NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                         "ieee8021x",
+                         NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                         "open",
+                         NULL);
+
+            if (s_8021x) {
+                /* Dynamic WEP requires a valid 802.1x setting since we can't
+                 * autocomplete 802.1x.
+                 */
+                if (!nm_setting_verify(NM_SETTING(s_8021x), NULL, error))
+                    return FALSE;
+            }
+        } else {
+            /* Static WEP */
+            g_object_set(s_wsec, NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "none", NULL);
+        }
+
+        return TRUE;
+    }
+
+    /* WPA/RSN */
+    g_assert(ap_wpa_flags || ap_rsn_flags);
+
+    /* Ensure key management is valid for WPA */
+    if ((key_mgmt && !strcmp(key_mgmt, "ieee8021x")) || leap_username) {
+        g_set_error_literal(
+            error,
+            NM_CONNECTION_ERROR,
+            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+            _("WPA authentication is incompatible with non-EAP (original) LEAP or Dynamic WEP"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_KEY_MGMT);
+        return FALSE;
+    }
+
+    /* 'shared' auth incompatible with any type of WPA */
+    if (auth_alg && strcmp(auth_alg, "open")) {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                            _("WPA authentication is incompatible with Shared Key authentication"));
+        g_prefix_error(error,
+                       "%s.%s: ",
+                       NM_SETTING_WIRELESS_SECURITY_SETTING_NAME,
+                       NM_SETTING_WIRELESS_SECURITY_AUTH_ALG);
+        return FALSE;
+    }
+
+    if (!verify_no_wep(s_wsec, "WPA", error))
+        return FALSE;
+
+    if (!verify_wpa_psk(s_wsec, s_8021x, adhoc, ap_wpa_flags, ap_rsn_flags, error))
+        return FALSE;
+
+    if (!adhoc && !verify_wpa_eap(s_wsec, s_8021x, ap_wpa_flags, ap_rsn_flags, error))
+        return FALSE;
+
+    if (adhoc) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "wpa-psk",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+        nm_setting_wireless_security_add_proto(s_wsec, "rsn");
+        nm_setting_wireless_security_add_pairwise(s_wsec, "ccmp");
+        nm_setting_wireless_security_add_group(s_wsec, "ccmp");
+    } else if (s_8021x) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "wpa-eap",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+        /* Leave proto/pairwise/group as client set them; if they are unset the
+         * supplicant will figure out the best combination at connect time.
+         */
+
+        /* 802.1x also requires the client to completely fill in the 8021x
+         * setting.  Since there's so much configuration required for it, there's
+         * no way it can be automatically completed.
+         */
+    } else if ((key_mgmt && !strcmp(key_mgmt, "sae"))
+               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE)) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "sae",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+    } else if ((key_mgmt && !strcmp(key_mgmt, "owe"))
+               || NM_FLAGS_ANY(ap_rsn_flags,
+                               NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "owe",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+    } else if ((key_mgmt && !strcmp(key_mgmt, "wpa-psk"))
+               || (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)
+               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK)) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "wpa-psk",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+        /* Leave proto/pairwise/group as client set them; if they are unset the
+         * supplicant will figure out the best combination at connect time.
+         */
+    } else if ((key_mgmt && !strcmp(key_mgmt, "wpa-eap-suite-b-192"))
+               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_EAP_SUITE_B_192)) {
+        g_object_set(s_wsec,
+                     NM_SETTING_WIRELESS_SECURITY_KEY_MGMT,
+                     "wpa-eap-suite-b-192",
+                     NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
+                     "open",
+                     NULL);
+    } else {
+        g_set_error_literal(error,
+                            NM_CONNECTION_ERROR,
+                            NM_CONNECTION_ERROR_FAILED,
+                            _("Failed to determine AP security information"));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+gboolean
+nm_wifi_utils_is_manf_default_ssid(GBytes *ssid)
+{
+    const guint8 *ssid_p;
+    gsize         ssid_l;
+    int           i;
+    /*
+     * List of manufacturer default SSIDs that are often unchanged by users.
+     *
+     * NOTE: this list should *not* contain networks that you would like to
+     * automatically roam to like "Starbucks" or "AT&T" or "T-Mobile HotSpot".
+     */
+    static const char *manf_defaults[] = {
+        "linksys",
+        "linksys-a",
+        "linksys-g",
+        "default",
+        "belkin54g",
+        "NETGEAR",
+        "o2DSL",
+        "WLAN",
+        "ALICE-WLAN",
+        "Speedport W 501V",
+        "TURBONETT",
+    };
+
+    ssid_p = g_bytes_get_data(ssid, &ssid_l);
+
+    for (i = 0; i < G_N_ELEMENTS(manf_defaults); i++) {
+        if (ssid_l == strlen(manf_defaults[i])) {
+            if (memcmp(manf_defaults[i], ssid_p, ssid_l) == 0)
+                return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+/* To be used for connections where the SSID has been validated before */
+gboolean
+nm_wifi_connection_get_iwd_ssid_and_security(NMConnection *        connection,
+                                             char **               ssid,
+                                             NMIwdNetworkSecurity *security)
+{
+    NMSettingWireless *        s_wireless;
+    NMSettingWirelessSecurity *s_wireless_sec;
+    const char *               key_mgmt = NULL;
+
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    if (!s_wireless)
+        return FALSE;
+
+    if (ssid) {
+        GBytes *    bytes = nm_setting_wireless_get_ssid(s_wireless);
+        gsize       ssid_len;
+        const char *ssid_str = (const char *) g_bytes_get_data(bytes, &ssid_len);
+
+        nm_assert(bytes && g_utf8_validate(ssid_str, ssid_len, NULL));
+        NM_SET_OUT(ssid, g_strndup(ssid_str, ssid_len));
+    }
+
+    if (!security)
+        return TRUE;
+
+    s_wireless_sec = nm_connection_get_setting_wireless_security(connection);
+    if (!s_wireless_sec) {
+        NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_OPEN);
+        return TRUE;
+    }
+
+    key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wireless_sec);
+    nm_assert(key_mgmt);
+
+    if (NM_IN_STRSET(key_mgmt, "none", "ieee8021x"))
+        NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_WEP);
+    else if (nm_streq(key_mgmt, "owe"))
+        NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_OPEN);
+    else if (NM_IN_STRSET(key_mgmt, "wpa-psk", "sae"))
+        NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_PSK);
+    else if (nm_streq(key_mgmt, "wpa-eap"))
+        NM_SET_OUT(security, NM_IWD_NETWORK_SECURITY_8021X);
+    else
+        return FALSE;
+
+    return TRUE;
+}
diff --git a/src/core/devices/wifi/nm-wifi-utils.h b/src/core/devices/wifi/nm-wifi-utils.h
new file mode 100644
index 00000000..474bea41
--- /dev/null
+++ b/src/core/devices/wifi/nm-wifi-utils.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#ifndef __NM_WIFI_UTILS_H__
+#define __NM_WIFI_UTILS_H__
+
+#include "nm-dbus-interface.h"
+#include "nm-connection.h"
+#include "nm-setting-wireless.h"
+#include "nm-setting-wireless-security.h"
+#include "nm-setting-8021x.h"
+
+typedef enum {
+    NM_IWD_NETWORK_SECURITY_OPEN,
+    NM_IWD_NETWORK_SECURITY_WEP,
+    NM_IWD_NETWORK_SECURITY_PSK,
+    NM_IWD_NETWORK_SECURITY_8021X,
+} NMIwdNetworkSecurity;
+
+gboolean nm_wifi_utils_complete_connection(GBytes *      ssid,
+                                           const char *  bssid,
+                                           NM80211Mode   mode,
+                                           guint32       ap_freq,
+                                           guint32       flags,
+                                           guint32       wpa_flags,
+                                           guint32       rsn_flags,
+                                           NMConnection *connection,
+                                           gboolean      lock_bssid,
+                                           GError **     error);
+
+gboolean nm_wifi_utils_is_manf_default_ssid(GBytes *ssid);
+
+gboolean nm_wifi_connection_get_iwd_ssid_and_security(NMConnection *        connection,
+                                                      char **               ssid,
+                                                      NMIwdNetworkSecurity *security);
+
+#endif /* __NM_WIFI_UTILS_H__ */
diff --git a/src/core/devices/wifi/tests/test-devices-wifi.c b/src/core/devices/wifi/tests/test-devices-wifi.c
new file mode 100644
index 00000000..bc0ba126
--- /dev/null
+++ b/src/core/devices/wifi/tests/test-devices-wifi.c
@@ -0,0 +1,1609 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "devices/wifi/nm-wifi-utils.h"
+#include "devices/wifi/nm-device-wifi.h"
+#include "nm-core-internal.h"
+
+#include "nm-test-utils-core.h"
+
+#define DEBUG 1
+
+/*****************************************************************************/
+
+#define COMPARE(src, expected, success, error, edomain, ecode)                             \
+    {                                                                                      \
+        if (expected) {                                                                    \
+            if (!success) {                                                                \
+                g_assert(error != NULL);                                                   \
+                g_warning("Failed to complete connection: %s", error->message);            \
+            }                                                                              \
+            g_assert(success == TRUE);                                                     \
+            g_assert(error == NULL);                                                       \
+                                                                                           \
+            success = nm_connection_compare(src, expected, NM_SETTING_COMPARE_FLAG_EXACT); \
+            if (success == FALSE && DEBUG) {                                               \
+                g_print("\n- COMPLETED ---------------------------------\n");              \
+                nm_connection_dump(src);                                                   \
+                g_print("+ EXPECTED ++++++++++++++++++++++++++++++++++++\n");              \
+                nm_connection_dump(expected);                                              \
+                g_print("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n");              \
+            }                                                                              \
+            g_assert(success == TRUE);                                                     \
+        } else {                                                                           \
+            if (success) {                                                                 \
+                g_print("\n- COMPLETED ---------------------------------\n");              \
+                nm_connection_dump(src);                                                   \
+            }                                                                              \
+            g_assert(success == FALSE);                                                    \
+            g_assert_error(error, edomain, ecode);                                         \
+        }                                                                                  \
+                                                                                           \
+        g_clear_error(&error);                                                             \
+    }
+
+static gboolean
+complete_connection(const char *  ssid,
+                    const char *  bssid,
+                    NM80211Mode   mode,
+                    guint32       flags,
+                    guint32       wpa_flags,
+                    guint32       rsn_flags,
+                    gboolean      lock_bssid,
+                    NMConnection *src,
+                    GError **     error)
+{
+    gs_unref_bytes GBytes *ssid_b = NULL;
+    NMSettingWireless *    s_wifi;
+
+    /* Add a wifi setting if one doesn't exist */
+    s_wifi = nm_connection_get_setting_wireless(src);
+    if (!s_wifi) {
+        s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+        nm_connection_add_setting(src, NM_SETTING(s_wifi));
+    }
+
+    ssid_b = g_bytes_new(ssid, strlen(ssid));
+
+    return nm_wifi_utils_complete_connection(ssid_b,
+                                             bssid,
+                                             mode,
+                                             0,
+                                             flags,
+                                             wpa_flags,
+                                             rsn_flags,
+                                             src,
+                                             lock_bssid,
+                                             error);
+}
+
+typedef struct {
+    const char *key;
+    const char *str;
+    guint32     uint;
+} KeyData;
+
+static void
+set_items(NMSetting *setting, const KeyData *items)
+{
+    const KeyData *item;
+    GParamSpec *   pspec;
+    GBytes *       tmp;
+
+    for (item = items; item && item->key; item++) {
+        g_assert(item->key);
+        pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), item->key);
+        g_assert(pspec);
+
+        if (pspec->value_type == G_TYPE_STRING) {
+            g_assert(item->uint == 0);
+            if (item->str)
+                g_object_set(G_OBJECT(setting), item->key, item->str, NULL);
+        } else if (pspec->value_type == G_TYPE_UINT) {
+            g_assert(item->str == NULL);
+            g_object_set(G_OBJECT(setting), item->key, item->uint, NULL);
+        } else if (pspec->value_type == G_TYPE_INT) {
+            int foo = (int) item->uint;
+
+            g_assert(item->str == NULL);
+            g_object_set(G_OBJECT(setting), item->key, foo, NULL);
+        } else if (pspec->value_type == G_TYPE_BOOLEAN) {
+            gboolean foo = !!(item->uint);
+
+            g_assert(item->str == NULL);
+            g_object_set(G_OBJECT(setting), item->key, foo, NULL);
+        } else if (pspec->value_type == G_TYPE_BYTES) {
+            g_assert(item->str);
+            tmp = g_bytes_new(item->str, strlen(item->str));
+            g_object_set(G_OBJECT(setting), item->key, tmp, NULL);
+            g_bytes_unref(tmp);
+        } else {
+            /* Special types, check based on property name */
+            if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_PROTO))
+                nm_setting_wireless_security_add_proto(NM_SETTING_WIRELESS_SECURITY(setting),
+                                                       item->str);
+            else if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_PAIRWISE))
+                nm_setting_wireless_security_add_pairwise(NM_SETTING_WIRELESS_SECURITY(setting),
+                                                          item->str);
+            else if (!strcmp(item->key, NM_SETTING_WIRELESS_SECURITY_GROUP))
+                nm_setting_wireless_security_add_group(NM_SETTING_WIRELESS_SECURITY(setting),
+                                                       item->str);
+            else if (!strcmp(item->key, NM_SETTING_802_1X_EAP))
+                nm_setting_802_1x_add_eap_method(NM_SETTING_802_1X(setting), item->str);
+        }
+    }
+}
+
+static NMSettingWireless *
+fill_wifi_empty(NMConnection *connection)
+{
+    NMSettingWireless *s_wifi;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    if (!s_wifi) {
+        s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wifi));
+    }
+    return s_wifi;
+}
+
+static NMSettingWireless *
+fill_wifi(NMConnection *connection, const KeyData items[])
+{
+    NMSettingWireless *s_wifi;
+
+    s_wifi = nm_connection_get_setting_wireless(connection);
+    if (!s_wifi) {
+        s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wifi));
+    }
+
+    set_items(NM_SETTING(s_wifi), items);
+    return s_wifi;
+}
+
+static NMSettingWirelessSecurity *
+fill_wsec(NMConnection *connection, const KeyData items[])
+{
+    NMSettingWirelessSecurity *s_wsec;
+
+    s_wsec = nm_connection_get_setting_wireless_security(connection);
+    if (!s_wsec) {
+        s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_wsec));
+    }
+
+    set_items(NM_SETTING(s_wsec), items);
+    return s_wsec;
+}
+
+static NMSetting8021x *
+fill_8021x(NMConnection *connection, const KeyData items[])
+{
+    NMSetting8021x *s_8021x;
+
+    s_8021x = nm_connection_get_setting_802_1x(connection);
+    if (!s_8021x) {
+        s_8021x = (NMSetting8021x *) nm_setting_802_1x_new();
+        nm_connection_add_setting(connection, NM_SETTING(s_8021x));
+    }
+
+    set_items(NM_SETTING(s_8021x), items);
+    return s_8021x;
+}
+
+static NMConnection *
+create_basic(const char *ssid, const char *bssid, NM80211Mode mode)
+{
+    NMConnection *     connection;
+    NMSettingWireless *s_wifi = NULL;
+    GBytes *           tmp;
+
+    connection = nm_simple_connection_new();
+
+    s_wifi = (NMSettingWireless *) nm_setting_wireless_new();
+    nm_connection_add_setting(connection, NM_SETTING(s_wifi));
+
+    /* SSID */
+    tmp = g_bytes_new(ssid, strlen(ssid));
+    g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_SSID, tmp, NULL);
+    g_bytes_unref(tmp);
+
+    /* BSSID */
+    if (bssid)
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_BSSID, bssid, NULL);
+
+    if (mode == NM_802_11_MODE_INFRA)
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, "infrastructure", NULL);
+    else if (mode == NM_802_11_MODE_ADHOC)
+        g_object_set(G_OBJECT(s_wifi), NM_SETTING_WIRELESS_MODE, "adhoc", NULL);
+    else
+        g_assert_not_reached();
+
+    return connection;
+}
+
+/*****************************************************************************/
+
+static void
+test_lock_bssid(void)
+{
+    NMConnection *src, *expected;
+    const char *  bssid = "01:02:03:04:05:06";
+    const char *  ssid  = "blahblah";
+    gboolean      success;
+    GError *      error = NULL;
+
+    src      = nm_simple_connection_new();
+    success  = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  TRUE,
+                                  src,
+                                  &error);
+    expected = create_basic(ssid, bssid, NM_802_11_MODE_INFRA);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_open_ap_empty_connection(void)
+{
+    NMConnection *src, *expected;
+    const char *  bssid = "01:02:03:04:05:06";
+    const char *  ssid  = "blahblah";
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an empty source connection is correctly filled with the
+     * SSID and Infra modes of the given AP details.
+     */
+
+    src      = nm_simple_connection_new();
+    success  = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_open_ap_leap_connection_1(gconstpointer add_wifi)
+{
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, "Bill Smith", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that a basic connection filled with a LEAP username is
+     * rejected when completion is attempted with an open AP.  LEAP requires
+     * the AP to have the Privacy bit set.
+     */
+
+    src = nm_simple_connection_new();
+    if (add_wifi)
+        fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_open_ap_leap_connection_2(void)
+{
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that a basic connection specifying IEEE8021x security (ie, Dynamic
+     * WEP or LEAP) is rejected when completion is attempted with an open AP.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_open_ap_wep_connection(gconstpointer add_wifi)
+{
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {
+        {NM_SETTING_WIRELESS_SECURITY_WEP_KEY0, "11111111111111111111111111", 0},
+        {NM_SETTING_WIRELESS_SECURITY_WEP_TX_KEYIDX, NULL, 0},
+        {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+        {NULL}};
+    gboolean success;
+    GError * error = NULL;
+
+    /* Test that a static WEP connection is rejected when completion is
+     * attempted with an open AP.
+     */
+
+    src = nm_simple_connection_new();
+    if (add_wifi)
+        fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_SETTING);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_ap_wpa_psk_connection_base(const char *  key_mgmt,
+                                const char *  auth_alg,
+                                guint32       flags,
+                                guint32       wpa_flags,
+                                guint32       rsn_flags,
+                                gboolean      add_wifi,
+                                guint         error_code,
+                                NMConnection *expected)
+{
+    NMConnection *src;
+    const char *  ssid        = "blahblah";
+    const char *  bssid       = "01:02:03:04:05:06";
+    const KeyData exp_wifi[]  = {{NM_SETTING_WIRELESS_SSID, ssid, 0},
+                                {NM_SETTING_WIRELESS_MODE, "infrastructure", 0},
+                                {NULL}};
+    const KeyData both_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0},
+                                 {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0},
+                                 {NM_SETTING_WIRELESS_SECURITY_PSK, "asdfasdfasdfasdfasdfafs", 0},
+                                 {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    src = nm_simple_connection_new();
+    if (add_wifi)
+        fill_wifi_empty(src);
+    fill_wsec(src, both_wsec);
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  flags,
+                                  wpa_flags,
+                                  rsn_flags,
+                                  FALSE,
+                                  src,
+                                  &error);
+    if (expected) {
+        fill_wifi(expected, exp_wifi);
+        fill_wsec(expected, both_wsec);
+    }
+    COMPARE(src, expected, success, error, NM_CONNECTION_ERROR, error_code);
+
+    g_object_unref(src);
+}
+
+static void
+test_open_ap_wpa_psk_connection_1(void)
+{
+    /* Test that a WPA-PSK connection filling only the PSK itself and *not*
+     * filling the wifi setting is rejected when completion is attempted with
+     * an open AP.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                                    NULL);
+}
+
+static void
+test_open_ap_wpa_psk_connection_2(void)
+{
+    /* Test that a WPA-PSK connection filling only the PSK itself and also
+     * filling the wifi setting is rejected when completion is attempted with
+     * an open AP.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    TRUE,
+                                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                                    NULL);
+}
+
+static void
+test_open_ap_wpa_psk_connection_3(void)
+{
+    /* Test that a WPA-PSK connection filling the PSK and setting the auth alg
+     * to 'open' is rejected when completion is attempted with an open AP.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "open",
+                                    NM_802_11_AP_FLAGS_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                                    NULL);
+}
+
+static void
+test_open_ap_wpa_psk_connection_4(void)
+{
+    /* Test that a WPA-PSK connection filling the PSK and setting the auth alg
+     * to 'shared' is rejected when completion is attempted with an open AP.
+     * Shared auth cannot be used with WPA.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "shared",
+                                    NM_802_11_AP_FLAGS_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                                    NULL);
+}
+
+static void
+test_open_ap_wpa_psk_connection_5(void)
+{
+    /* Test that a WPA-PSK connection filling the PSK, the auth algorithm, and
+     * key management is rejected when completion is attempted with an open AP.
+     */
+    test_ap_wpa_psk_connection_base("wpa-psk",
+                                    "open",
+                                    NM_802_11_AP_FLAGS_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_SETTING,
+                                    NULL);
+}
+
+/*****************************************************************************/
+
+static void
+test_ap_wpa_eap_connection_base(const char *key_mgmt,
+                                const char *auth_alg,
+                                guint32     flags,
+                                guint32     wpa_flags,
+                                guint32     rsn_flags,
+                                gboolean    add_wifi,
+                                guint       error_code)
+{
+    NMConnection *src;
+    const char *  bssid       = "01:02:03:04:05:06";
+    const KeyData src_empty[] = {{NULL}};
+    const KeyData src_wsec[]  = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, key_mgmt, 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, auth_alg, 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    src = nm_simple_connection_new();
+    if (add_wifi)
+        fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    fill_8021x(src, src_empty);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  flags,
+                                  wpa_flags,
+                                  rsn_flags,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* Failure expected */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, error_code);
+
+    g_object_unref(src);
+}
+
+enum {
+    IDX_NONE = 0,
+    IDX_OPEN,
+    IDX_PRIV,
+    IDX_WPA_PSK_PTKIP_GTKIP,
+    IDX_WPA_PSK_PTKIP_PCCMP_GTKIP,
+    IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP,
+    IDX_WPA_RSN_PSK_PCCMP_GCCMP,
+    IDX_RSN_PSK_PCCMP_GCCMP,
+    IDX_RSN_PSK_PTKIP_PCCMP_GTKIP,
+    IDX_WPA_8021X,
+    IDX_RSN_8021X,
+};
+
+static guint32
+flags_for_idx(guint32 idx)
+{
+    if (idx == IDX_OPEN)
+        return NM_802_11_AP_FLAGS_NONE;
+    else if (idx == IDX_PRIV || idx == IDX_WPA_PSK_PTKIP_GTKIP
+             || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_RSN_PSK_PCCMP_GCCMP
+             || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP
+             || idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP || idx == IDX_WPA_8021X || idx == IDX_RSN_8021X)
+        return NM_802_11_AP_FLAGS_PRIVACY;
+    else
+        g_assert_not_reached();
+}
+
+static guint32
+wpa_flags_for_idx(guint32 idx)
+{
+    if (idx == IDX_OPEN || idx == IDX_PRIV || idx == IDX_RSN_8021X || idx == IDX_RSN_PSK_PCCMP_GCCMP
+        || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP)
+        return NM_802_11_AP_SEC_NONE;
+    else if (idx == IDX_WPA_PSK_PTKIP_GTKIP)
+        return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_TKIP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP)
+        return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (NM_IN_SET(idx, IDX_WPA_PSK_PTKIP_PCCMP_GTKIP, IDX_WPA_RSN_PSK_PCCMP_GCCMP))
+        return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_WPA_8021X)
+        return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_GROUP_TKIP
+               | NM_802_11_AP_SEC_KEY_MGMT_802_1X;
+    else
+        g_assert_not_reached();
+}
+
+static guint32
+rsn_flags_for_idx(guint32 idx)
+{
+    if (idx == IDX_OPEN || idx == IDX_PRIV || idx == IDX_WPA_8021X || idx == IDX_WPA_PSK_PTKIP_GTKIP
+        || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP)
+        return NM_802_11_AP_SEC_NONE;
+    else if (idx == IDX_RSN_PSK_PCCMP_GCCMP)
+        return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP)
+        return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP)
+        return NM_802_11_AP_SEC_PAIR_TKIP | NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_TKIP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP)
+        return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP
+               | NM_802_11_AP_SEC_KEY_MGMT_PSK;
+    else if (idx == IDX_RSN_8021X)
+        return NM_802_11_AP_SEC_PAIR_CCMP | NM_802_11_AP_SEC_GROUP_CCMP
+               | NM_802_11_AP_SEC_KEY_MGMT_802_1X;
+    else
+        g_assert_not_reached();
+}
+
+static guint32
+error_code_for_idx(guint32 idx, guint num)
+{
+    if (idx == IDX_OPEN)
+        return NM_CONNECTION_ERROR_INVALID_SETTING;
+    else if (idx == IDX_PRIV) {
+        if (num <= 3)
+            return NM_CONNECTION_ERROR_MISSING_PROPERTY;
+        else
+            return NM_CONNECTION_ERROR_INVALID_PROPERTY;
+    } else if (idx == IDX_WPA_PSK_PTKIP_GTKIP || idx == IDX_WPA_PSK_PTKIP_PCCMP_GTKIP
+               || idx == IDX_WPA_RSN_PSK_PCCMP_GCCMP || idx == IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP
+               || idx == IDX_RSN_PSK_PTKIP_PCCMP_GTKIP || idx == IDX_RSN_PSK_PCCMP_GCCMP)
+        if (num == 4)
+            return NM_CONNECTION_ERROR_INVALID_PROPERTY;
+        else
+            return NM_CONNECTION_ERROR_INVALID_SETTING;
+    else
+        g_assert_not_reached();
+}
+
+static void
+test_ap_wpa_eap_connection_1(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+
+    test_ap_wpa_eap_connection_base(NULL,
+                                    NULL,
+                                    flags_for_idx(idx),
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    error_code_for_idx(idx, 1));
+}
+
+static void
+test_ap_wpa_eap_connection_2(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+
+    test_ap_wpa_eap_connection_base(NULL,
+                                    NULL,
+                                    flags_for_idx(idx),
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    TRUE,
+                                    error_code_for_idx(idx, 2));
+}
+
+static void
+test_ap_wpa_eap_connection_3(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+
+    test_ap_wpa_eap_connection_base(NULL,
+                                    "open",
+                                    flags_for_idx(idx),
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    error_code_for_idx(idx, 3));
+}
+
+static void
+test_ap_wpa_eap_connection_4(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+
+    test_ap_wpa_eap_connection_base(NULL,
+                                    "shared",
+                                    flags_for_idx(idx),
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    error_code_for_idx(idx, 4));
+}
+
+static void
+test_ap_wpa_eap_connection_5(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+
+    test_ap_wpa_eap_connection_base("wpa-eap",
+                                    "open",
+                                    flags_for_idx(idx),
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    error_code_for_idx(idx, 5));
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_empty_connection(void)
+{
+    NMConnection *src, *expected;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const char *  ssid       = "blahblah";
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "none", 0}, {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an empty connection is completed to a valid Static WEP
+     * connection when completed with an AP with the Privacy bit set.
+     */
+
+    src     = nm_simple_connection_new();
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+
+    /* Static WEP connection expected */
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    fill_wsec(expected, exp_wsec);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_leap_connection_1(gconstpointer add_wifi)
+{
+    NMConnection *src, *expected;
+    const char *  ssid          = "blahblah";
+    const char *  bssid         = "01:02:03:04:05:06";
+    const char *  leap_username = "Bill Smith";
+    const KeyData src_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                {NULL}};
+    const KeyData exp_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an minimal LEAP connection specifying only key management and
+     * the LEAP username is completed to a full LEAP connection when completed
+     * with an AP with the Privacy bit set.
+     */
+
+    src = nm_simple_connection_new();
+    if (add_wifi)
+        fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect success here; since LEAP APs just set the 'privacy' flag
+     * there's no way to determine from the AP's beacon whether it's static WEP,
+     * dynamic WEP, or LEAP.
+     */
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    fill_wsec(expected, exp_wsec);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_leap_connection_2(void)
+{
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an minimal LEAP connection specifying only key management and
+     * the LEAP auth alg is completed to a full LEAP connection when completed
+     * with an AP with the Privacy bit set.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure here, we need a LEAP username */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_MISSING_PROPERTY);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_dynamic_wep_1(void)
+{
+    NMConnection *src, *expected;
+    const char *  ssid         = "blahblah";
+    const char *  bssid        = "01:02:03:04:05:06";
+    const KeyData src_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+    const KeyData both_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0},
+                                  {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0},
+                                  {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0},
+                                  {NULL}};
+    const KeyData exp_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an minimal Dynamic WEP connection specifying key management,
+     * the auth algorithm, and valid 802.1x setting is completed to a valid
+     * Dynamic WEP connection when completed with an AP with the Privacy bit set.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    fill_8021x(src, both_8021x);
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+
+    /* We expect a completed Dynamic WEP connection */
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    fill_wsec(expected, exp_wsec);
+    fill_8021x(expected, both_8021x);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_dynamic_wep_2(void)
+{
+    NMConnection *src, *expected;
+    const char *  ssid         = "blahblah";
+    const char *  bssid        = "01:02:03:04:05:06";
+    const KeyData src_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0}, {NULL}};
+    const KeyData both_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0},
+                                  {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0},
+                                  {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0},
+                                  {NULL}};
+    const KeyData exp_wsec[]   = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that an minimal Dynamic WEP connection specifying only the auth
+     * algorithm and a valid 802.1x setting is completed to a valid Dynamic
+     * WEP connection when completed with an AP with the Privacy bit set.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    fill_8021x(src, both_8021x);
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+
+    /* We expect a completed Dynamic WEP connection */
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    fill_wsec(expected, exp_wsec);
+    fill_8021x(expected, both_8021x);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_dynamic_wep_3(void)
+{
+    NMConnection *src;
+    const char *  bssid       = "01:02:03:04:05:06";
+    const KeyData src_wsec[]  = {{NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "shared", 0}, {NULL}};
+    const KeyData src_8021x[] = {{NM_SETTING_802_1X_EAP, "peap", 0},
+                                 {NM_SETTING_802_1X_IDENTITY, "Bill Smith", 0},
+                                 {NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", 0},
+                                 {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Ensure that a basic connection specifying 'shared' auth and an 802.1x
+     * setting is rejected, as 802.1x is incompatible with 'shared' auth.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    fill_8021x(src, src_8021x);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  NM_802_11_AP_SEC_NONE,
+                                  NM_802_11_AP_SEC_NONE,
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* Expect failure; shared is not compatible with dynamic WEP */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_priv_ap_wpa_psk_connection_1(void)
+{
+    /* Test that a basic WPA-PSK connection is rejected when completion is
+     * attempted with an AP with just the Privacy bit set.  Lack of WPA/RSN
+     * flags means the AP provides Static/Dynamic WEP or LEAP, not WPA.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+static void
+test_priv_ap_wpa_psk_connection_2(void)
+{
+    /* Test that a basic WPA-PSK connection is rejected when completion is
+     * attempted with an AP with just the Privacy bit set.  Lack of WPA/RSN
+     * flags means the AP provides Static/Dynamic WEP or LEAP, not WPA.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    TRUE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+static void
+test_priv_ap_wpa_psk_connection_3(void)
+{
+    /* Test that a basic WPA-PSK connection specifying only the auth algorithm
+     * is rejected when completion is attempted with an AP with just the Privacy
+     * bit set.  Lack of WPA/RSN flags means the AP provides Static/Dynamic WEP
+     * or LEAP, not WPA.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "open",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+static void
+test_priv_ap_wpa_psk_connection_4(void)
+{
+    /* Test that a basic WPA-PSK connection specifying only the auth algorithm
+     * is rejected when completion is attempted with an AP with just the Privacy
+     * bit set.  Lack of WPA/RSN flags means the AP provides Static/Dynamic WEP
+     * or LEAP, not WPA.  Second, 'shared' auth is incompatible with WPA.
+     */
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "shared",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+static void
+test_priv_ap_wpa_psk_connection_5(void)
+{
+    /* Test that a WPA-PSK connection specifying both the key management and
+     * auth algorithm is rejected when completion is attempted with an AP with
+     * just the Privacy bit set.  Lack of WPA/RSN flags means the AP provides
+     * Static/Dynamic WEP or LEAP, not WPA.
+     */
+    test_ap_wpa_psk_connection_base("wpa-psk",
+                                    "open",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    NM_802_11_AP_SEC_NONE,
+                                    NM_802_11_AP_SEC_NONE,
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+/*****************************************************************************/
+
+static void
+test_wpa_ap_empty_connection(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *src, *expected;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const char *  ssid       = "blahblah";
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that a basic WPA-PSK connection specifying just key management and
+     * the auth algorithm is completed successfully when given an AP with WPA
+     * or RSN flags.
+     */
+
+    src     = nm_simple_connection_new();
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  wpa_flags_for_idx(idx),
+                                  rsn_flags_for_idx(idx),
+                                  FALSE,
+                                  src,
+                                  &error);
+
+    /* WPA connection expected */
+    expected = create_basic(ssid, NULL, NM_802_11_MODE_INFRA);
+    fill_wsec(expected, exp_wsec);
+    COMPARE(src, expected, success, error, 0, 0);
+
+    g_object_unref(src);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_wpa_ap_leap_connection_1(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *src;
+    const char *  ssid          = "blahblah";
+    const char *  bssid         = "01:02:03:04:05:06";
+    const char *  leap_username = "Bill Smith";
+    const KeyData src_wsec[]    = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_LEAP_USERNAME, leap_username, 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that completion of a LEAP connection with a WPA-enabled AP is
+     * rejected since WPA APs (usually) do not support LEAP.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection(ssid,
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  wpa_flags_for_idx(idx),
+                                  rsn_flags_for_idx(idx),
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* Expect failure here; WPA APs don't support old-school LEAP */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_wpa_ap_leap_connection_2(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "leap", 0},
+                                {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that completion of a LEAP connection with a WPA-enabled AP is
+     * rejected since WPA APs (usually) do not support LEAP.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  wpa_flags_for_idx(idx),
+                                  rsn_flags_for_idx(idx),
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure here, we need a LEAP username */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_wpa_ap_dynamic_wep_connection(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *src;
+    const char *  bssid      = "01:02:03:04:05:06";
+    const KeyData src_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "ieee8021x", 0}, {NULL}};
+    gboolean      success;
+    GError *      error = NULL;
+
+    /* Test that completion of a Dynamic WEP connection with a WPA-enabled AP is
+     * rejected since WPA APs (usually) do not support Dynamic WEP.
+     */
+
+    src = nm_simple_connection_new();
+    fill_wifi_empty(src);
+    fill_wsec(src, src_wsec);
+    success = complete_connection("blahblah",
+                                  bssid,
+                                  NM_802_11_MODE_INFRA,
+                                  NM_802_11_AP_FLAGS_PRIVACY,
+                                  wpa_flags_for_idx(idx),
+                                  rsn_flags_for_idx(idx),
+                                  FALSE,
+                                  src,
+                                  &error);
+    /* We expect failure here since Dynamic WEP is incompatible with WPA */
+    COMPARE(src, NULL, success, error, NM_CONNECTION_ERROR, NM_CONNECTION_ERROR_INVALID_PROPERTY);
+
+    g_object_unref(src);
+}
+
+/*****************************************************************************/
+
+static void
+test_wpa_ap_wpa_psk_connection_1(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *expected;
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+
+    expected = nm_simple_connection_new();
+    fill_wsec(expected, exp_wsec);
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    expected);
+    g_object_unref(expected);
+}
+
+static void
+test_wpa_ap_wpa_psk_connection_2(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *expected;
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+
+    expected = nm_simple_connection_new();
+    fill_wsec(expected, exp_wsec);
+    test_ap_wpa_psk_connection_base(NULL,
+                                    NULL,
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    TRUE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    expected);
+    g_object_unref(expected);
+}
+
+static void
+test_wpa_ap_wpa_psk_connection_3(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *expected;
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+
+    expected = nm_simple_connection_new();
+    fill_wsec(expected, exp_wsec);
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "open",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    expected);
+    g_object_unref(expected);
+}
+
+static void
+test_wpa_ap_wpa_psk_connection_4(gconstpointer data)
+{
+    guint idx = GPOINTER_TO_UINT(data);
+    test_ap_wpa_psk_connection_base(NULL,
+                                    "shared",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    NULL);
+}
+
+static void
+test_wpa_ap_wpa_psk_connection_5(gconstpointer data)
+{
+    guint         idx = GPOINTER_TO_UINT(data);
+    NMConnection *expected;
+    const KeyData exp_wsec[] = {{NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk", 0},
+                                {NM_SETTING_WIRELESS_SECURITY_AUTH_ALG, "open", 0},
+                                {NULL}};
+
+    expected = nm_simple_connection_new();
+    fill_wsec(expected, exp_wsec);
+    test_ap_wpa_psk_connection_base("wpa-psk",
+                                    "open",
+                                    NM_802_11_AP_FLAGS_PRIVACY,
+                                    wpa_flags_for_idx(idx),
+                                    rsn_flags_for_idx(idx),
+                                    FALSE,
+                                    NM_CONNECTION_ERROR_INVALID_PROPERTY,
+                                    expected);
+    g_object_unref(expected);
+}
+
+/*****************************************************************************/
+
+static void
+test_strength_dbm(void)
+{
+    /* boundary conditions first */
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-1), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-40), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-30), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-100), ==, 0);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-200), ==, 0);
+
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-81), ==, 32);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-92), ==, 14);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-74), ==, 44);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-81), ==, 32);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(-66), ==, 57);
+}
+
+static void
+test_strength_percent(void)
+{
+    int i;
+
+    /* boundary conditions first */
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(0), ==, 0);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(100), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(110), ==, 100);
+
+    for (i = 0; i <= 100; i++)
+        g_assert_cmpint(nm_wifi_utils_level_to_quality(i), ==, i);
+}
+
+static void
+test_strength_wext(void)
+{
+    /* boundary conditions that we assume aren't WEXT first */
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(256), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(110), ==, 100);
+
+    /* boundary conditions that we assume are WEXT */
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(111), ==, 0);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(150), ==, 0);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(225), ==, 100);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(255), ==, 100);
+
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(157), ==, 2);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(200), ==, 74);
+    g_assert_cmpint(nm_wifi_utils_level_to_quality(215), ==, 99);
+}
+
+#define _assert_strength_in_range(x)  \
+    ({                                \
+        guint32 _x = (x);             \
+        g_assert_cmpint(_x, >=, 0);   \
+        g_assert_cmpint(_x, <=, 100); \
+    })
+
+static void
+test_strength_all(void)
+{
+    int val;
+
+    for (val = -200; val < 300; val++)
+        _assert_strength_in_range(nm_wifi_utils_level_to_quality(val));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT32));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT32));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MININT16));
+    _assert_strength_in_range(nm_wifi_utils_level_to_quality(G_MAXINT16));
+}
+
+/*****************************************************************************/
+
+static void
+do_test_ssids_options_to_ptrarray(const char *const *ssids)
+{
+    GVariantBuilder  builder;
+    gs_unref_variant GVariant *variant     = NULL;
+    gs_unref_ptrarray GPtrArray *ssids_arr = NULL;
+    gs_free_error GError *error            = NULL;
+    gsize                 len;
+    gsize                 i;
+
+    g_assert(ssids);
+
+    len = NM_PTRARRAY_LEN(ssids);
+
+    g_variant_builder_init(&builder, G_VARIANT_TYPE("aay"));
+    for (i = 0; i < len; i++) {
+        const char *ssid = ssids[i];
+
+        g_variant_builder_add(
+            &builder,
+            "@ay",
+            g_variant_new_fixed_array(G_VARIANT_TYPE_BYTE, ssid, strlen(ssid), 1));
+    }
+    variant = g_variant_builder_end(&builder);
+
+    if (nmtst_get_rand_bool())
+        g_variant_ref_sink(variant);
+
+    ssids_arr = nmtst_ssids_options_to_ptrarray(variant, &error);
+    g_assert(!error);
+    if (len == 0) {
+        g_assert(!ssids_arr);
+        return;
+    }
+    g_assert_cmpint(len, ==, ssids_arr->len);
+    for (i = 0; i < len; i++) {
+        const char *ssid  = ssids[i];
+        GBytes *    bytes = ssids_arr->pdata[i];
+
+        g_assert(nm_utils_gbytes_equal_mem(bytes, ssid, strlen(ssid)));
+    }
+}
+
+static void
+test_ssids_options_to_ptrarray(void)
+{
+    do_test_ssids_options_to_ptrarray(NM_PTRARRAY_EMPTY(const char *));
+    do_test_ssids_options_to_ptrarray(NM_MAKE_STRV("ab"));
+    do_test_ssids_options_to_ptrarray(NM_MAKE_STRV("ab", "cd", "fsdfdsf"));
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    gsize i;
+
+    nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT");
+
+    g_test_add_func("/wifi/lock_bssid", test_lock_bssid);
+
+    /* Open AP tests; make sure that connections to be completed that have
+     * various security-related settings already set cause the completion
+     * to fail.
+     */
+    g_test_add_func("/wifi/open_ap/empty_connection", test_open_ap_empty_connection);
+    g_test_add_data_func("/wifi/open_ap/leap_connection/1",
+                         (gconstpointer) TRUE,
+                         test_open_ap_leap_connection_1);
+    g_test_add_data_func("/wifi/open_ap/leap_connection/1_no_add_wifi",
+                         (gconstpointer) FALSE,
+                         test_open_ap_leap_connection_1);
+    g_test_add_func("/wifi/open_ap/leap_connection/2", test_open_ap_leap_connection_2);
+    g_test_add_data_func("/wifi/open_ap/wep_connection_true",
+                         (gconstpointer) TRUE,
+                         test_open_ap_wep_connection);
+    g_test_add_data_func("/wifi/open_ap/wep_connection_false",
+                         (gconstpointer) FALSE,
+                         test_open_ap_wep_connection);
+
+    g_test_add_func("/wifi/open_ap/wpa_psk_connection/1", test_open_ap_wpa_psk_connection_1);
+    g_test_add_func("/wifi/open_ap/wpa_psk_connection/2", test_open_ap_wpa_psk_connection_2);
+    g_test_add_func("/wifi/open_ap/wpa_psk_connection/3", test_open_ap_wpa_psk_connection_3);
+    g_test_add_func("/wifi/open_ap/wpa_psk_connection/4", test_open_ap_wpa_psk_connection_4);
+    g_test_add_func("/wifi/open_ap/wpa_psk_connection/5", test_open_ap_wpa_psk_connection_5);
+
+    g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/1",
+                         (gconstpointer) IDX_OPEN,
+                         test_ap_wpa_eap_connection_1);
+    g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/2",
+                         (gconstpointer) IDX_OPEN,
+                         test_ap_wpa_eap_connection_2);
+    g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/3",
+                         (gconstpointer) IDX_OPEN,
+                         test_ap_wpa_eap_connection_3);
+    g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/4",
+                         (gconstpointer) IDX_OPEN,
+                         test_ap_wpa_eap_connection_4);
+    g_test_add_data_func("/wifi/open_ap/wpa_eap_connection/5",
+                         (gconstpointer) IDX_OPEN,
+                         test_ap_wpa_eap_connection_5);
+
+    /* WEP AP tests */
+    g_test_add_func("/wifi/priv_ap/empty_connection", test_priv_ap_empty_connection);
+    g_test_add_data_func("/wifi/priv_ap/leap_connection/1",
+                         (gconstpointer) FALSE,
+                         test_priv_ap_leap_connection_1);
+    g_test_add_func("/wifi/priv_ap/leap_connection/2", test_priv_ap_leap_connection_2);
+
+    g_test_add_func("/wifi/priv_ap/dynamic_wep/1", test_priv_ap_dynamic_wep_1);
+    g_test_add_func("/wifi/priv_ap/dynamic_wep/2", test_priv_ap_dynamic_wep_2);
+    g_test_add_func("/wifi/priv_ap/dynamic_wep/3", test_priv_ap_dynamic_wep_3);
+
+    g_test_add_func("/wifi/priv_ap/wpa_psk_connection/1", test_priv_ap_wpa_psk_connection_1);
+    g_test_add_func("/wifi/priv_ap/wpa_psk_connection/2", test_priv_ap_wpa_psk_connection_2);
+    g_test_add_func("/wifi/priv_ap/wpa_psk_connection/3", test_priv_ap_wpa_psk_connection_3);
+    g_test_add_func("/wifi/priv_ap/wpa_psk_connection/4", test_priv_ap_wpa_psk_connection_4);
+    g_test_add_func("/wifi/priv_ap/wpa_psk_connection/5", test_priv_ap_wpa_psk_connection_5);
+
+    g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/1",
+                         (gconstpointer) IDX_PRIV,
+                         test_ap_wpa_eap_connection_1);
+    g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/2",
+                         (gconstpointer) IDX_PRIV,
+                         test_ap_wpa_eap_connection_2);
+    g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/3",
+                         (gconstpointer) IDX_PRIV,
+                         test_ap_wpa_eap_connection_3);
+    g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/4",
+                         (gconstpointer) IDX_PRIV,
+                         test_ap_wpa_eap_connection_4);
+    g_test_add_data_func("/wifi/priv_ap/wpa_eap_connection/5",
+                         (gconstpointer) IDX_PRIV,
+                         test_ap_wpa_eap_connection_5);
+
+#define ADD_FUNC(func)                                                                  \
+    do {                                                                                \
+        char *name_idx = g_strdup_printf("/wifi/wpa_psk/" G_STRINGIFY(func) "/%zd", i); \
+        g_test_add_data_func(name_idx, (gconstpointer) i, func);                        \
+        g_free(name_idx);                                                               \
+    } while (0)
+
+    /* WPA-PSK tests */
+    for (i = IDX_WPA_PSK_PTKIP_GTKIP; i <= IDX_WPA_RSN_PSK_PCCMP_GCCMP; i++) {
+        ADD_FUNC(test_wpa_ap_empty_connection);
+        ADD_FUNC(test_wpa_ap_leap_connection_1);
+        ADD_FUNC(test_wpa_ap_leap_connection_2);
+        ADD_FUNC(test_wpa_ap_dynamic_wep_connection);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_1);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_2);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_3);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_4);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_5);
+        ADD_FUNC(test_ap_wpa_eap_connection_1);
+        ADD_FUNC(test_ap_wpa_eap_connection_2);
+        ADD_FUNC(test_ap_wpa_eap_connection_3);
+        ADD_FUNC(test_ap_wpa_eap_connection_4);
+        ADD_FUNC(test_ap_wpa_eap_connection_5);
+    }
+
+#undef ADD_FUNC
+#define ADD_FUNC(func)                                                                  \
+    do {                                                                                \
+        char *name_idx = g_strdup_printf("/wifi/rsn_psk/" G_STRINGIFY(func) "/%zd", i); \
+        g_test_add_data_func(name_idx, (gconstpointer) i, func);                        \
+        g_free(name_idx);                                                               \
+    } while (0)
+
+    /* RSN-PSK tests */
+    for (i = IDX_WPA_RSN_PSK_PTKIP_PCCMP_GTKIP; i <= IDX_RSN_PSK_PTKIP_PCCMP_GTKIP; i++) {
+        ADD_FUNC(test_wpa_ap_empty_connection);
+        ADD_FUNC(test_wpa_ap_leap_connection_1);
+        ADD_FUNC(test_wpa_ap_leap_connection_2);
+        ADD_FUNC(test_wpa_ap_dynamic_wep_connection);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_1);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_2);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_3);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_4);
+        ADD_FUNC(test_wpa_ap_wpa_psk_connection_5);
+        ADD_FUNC(test_ap_wpa_eap_connection_1);
+        ADD_FUNC(test_ap_wpa_eap_connection_2);
+        ADD_FUNC(test_ap_wpa_eap_connection_3);
+        ADD_FUNC(test_ap_wpa_eap_connection_4);
+        ADD_FUNC(test_ap_wpa_eap_connection_5);
+    }
+
+#undef ADD_FUNC
+
+    /* Scanned signal strength conversion tests */
+    g_test_add_func("/wifi/strength/dbm", test_strength_dbm);
+    g_test_add_func("/wifi/strength/percent", test_strength_percent);
+    g_test_add_func("/wifi/strength/wext", test_strength_wext);
+    g_test_add_func("/wifi/strength/all", test_strength_all);
+
+    g_test_add_func("/wifi/ssids_options_to_ptrarray", test_ssids_options_to_ptrarray);
+
+    return g_test_run();
+}
diff --git a/src/core/devices/wwan/libnm-wwan.ver b/src/core/devices/wwan/libnm-wwan.ver
new file mode 100644
index 00000000..c368a590
--- /dev/null
+++ b/src/core/devices/wwan/libnm-wwan.ver
@@ -0,0 +1,41 @@
+{
+global:
+	nm_modem_act_stage1_prepare;
+	nm_modem_act_stage2_config;
+	nm_modem_check_connection_compatible;
+	nm_modem_claim;
+	nm_modem_complete_connection;
+	nm_modem_deactivate;
+	nm_modem_deactivate_async;
+	nm_modem_device_state_changed;
+	nm_modem_get_apn;
+	nm_modem_get_capabilities;
+	nm_modem_get_configured_mtu;
+	nm_modem_get_control_port;
+	nm_modem_get_device_id;
+	nm_modem_get_driver;
+	nm_modem_get_iid;
+	nm_modem_get_ip_ifindex;
+	nm_modem_get_operator_code;
+	nm_modem_get_path;
+	nm_modem_get_secrets;
+	nm_modem_get_state;
+	nm_modem_get_type;
+	nm_modem_get_uid;
+	nm_modem_ip4_pre_commit;
+	nm_modem_is_claimed;
+	nm_modem_manager_get;
+	nm_modem_manager_get_modems;
+	nm_modem_manager_get_type;
+	nm_modem_manager_name_owner_get;
+	nm_modem_manager_name_owner_ref;
+	nm_modem_manager_name_owner_unref;
+	nm_modem_owns_port;
+	nm_modem_set_mm_enabled;
+	nm_modem_stage3_ip4_config_start;
+	nm_modem_stage3_ip6_config_start;
+	nm_modem_state_to_string;
+	nm_modem_unclaim;
+local:
+	*;
+};
diff --git a/src/core/devices/wwan/meson.build b/src/core/devices/wwan/meson.build
new file mode 100644
index 00000000..87af0429
--- /dev/null
+++ b/src/core/devices/wwan/meson.build
@@ -0,0 +1,88 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+wwan_inc = include_directories('.')
+
+linker_script = join_paths(meson.current_source_dir(), 'libnm-wwan.ver')
+
+libnm_wwan = shared_module(
+  'nm-wwan',
+  sources: files(
+  'nm-service-providers.c',
+  'nm-modem-broadband.c',
+  'nm-modem.c',
+  'nm-modem-manager.c',
+  ) + (enable_ofono ? files('nm-modem-ofono.c') : files()),
+  dependencies: [
+    core_plugin_dep,
+    libsystemd_dep,
+    mm_glib_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_args: '-Wl,--version-script,@0@'.format(linker_script),
+  link_depends: linker_script,
+  install: true,
+  install_dir: nm_plugindir,
+)
+
+libnm_wwan_dep = declare_dependency(
+  include_directories: wwan_inc,
+  link_with: libnm_wwan,
+)
+
+core_plugins += libnm_wwan
+
+test(
+  'check-wwan',
+  check_exports,
+  args: [
+    libnm_wwan.full_path(),
+    linker_script,
+  ],
+)
+
+libnm_device_plugin_wwan = shared_module(
+  'nm-device-plugin-wwan',
+  sources: files(
+    'nm-device-modem.c',
+    'nm-wwan-factory.c',
+  ),
+  dependencies: [
+    core_plugin_dep,
+    libsystemd_dep,
+    mm_glib_dep,
+  ],
+  c_args: daemon_c_flags,
+  link_with: libnm_wwan,
+  link_args: ldflags_linker_script_devices,
+  link_depends: linker_script_devices,
+  install: true,
+  install_dir: nm_plugindir,
+  install_rpath: nm_plugindir,
+)
+
+core_plugins += libnm_device_plugin_wwan
+
+run_target(
+  'check-local-devices-wwan',
+  command: [check_exports, libnm_device_plugin_wwan.full_path(), linker_script_devices],
+  depends: libnm_device_plugin_wwan,
+)
+
+if enable_tests
+  exe = executable(
+    'test-service-providers',
+    files(
+      'tests/test-service-providers.c',
+      'nm-service-providers.c',
+    ),
+    include_directories: wwan_inc,
+    dependencies: libNetworkManagerTest_dep,
+    c_args: test_c_flags,
+  )
+  test(
+    'wwan/test-service-providers',
+    test_script,
+    timeout: default_test_timeout,
+    args: test_args + [exe.full_path()],
+  )
+endif
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
new file mode 100644
index 00000000..3ea89d2c
--- /dev/null
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -0,0 +1,957 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-device-modem.h"
+
+#include "nm-modem.h"
+#include "nm-ip4-config.h"
+#include "devices/nm-device-private.h"
+#include "nm-rfkill-manager.h"
+#include "settings/nm-settings-connection.h"
+#include "nm-modem-broadband.h"
+#include "NetworkManagerUtils.h"
+#include "nm-core-internal.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceModem
+#include "devices/nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceModem,
+                             PROP_MODEM,
+                             PROP_CAPABILITIES,
+                             PROP_CURRENT_CAPABILITIES,
+                             PROP_DEVICE_ID,
+                             PROP_OPERATOR_CODE,
+                             PROP_APN, );
+
+typedef struct {
+    NMModem *                 modem;
+    NMDeviceModemCapabilities caps;
+    NMDeviceModemCapabilities current_caps;
+    char *                    device_id;
+    char *                    operator_code;
+    char *                    apn;
+    bool                      rf_enabled : 1;
+    NMDeviceStageState        stage1_state : 3;
+} NMDeviceModemPrivate;
+
+struct _NMDeviceModem {
+    NMDevice             parent;
+    NMDeviceModemPrivate _priv;
+};
+
+struct _NMDeviceModemClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceModem, nm_device_modem, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_MODEM_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceModem, NM_IS_DEVICE_MODEM, NMDevice)
+
+/*****************************************************************************/
+
+static void
+ppp_failed(NMModem *modem, guint i_reason, gpointer user_data)
+{
+    NMDevice *          device = NM_DEVICE(user_data);
+    NMDeviceModem *     self   = NM_DEVICE_MODEM(user_data);
+    NMDeviceStateReason reason = i_reason;
+
+    switch (nm_device_get_state(device)) {
+    case NM_DEVICE_STATE_PREPARE:
+    case NM_DEVICE_STATE_CONFIG:
+    case NM_DEVICE_STATE_NEED_AUTH:
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        break;
+    case NM_DEVICE_STATE_IP_CONFIG:
+    case NM_DEVICE_STATE_IP_CHECK:
+    case NM_DEVICE_STATE_SECONDARIES:
+    case NM_DEVICE_STATE_ACTIVATED:
+        if (nm_device_activate_ip4_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET);
+        else if (nm_device_activate_ip6_state_in_conf(device))
+            nm_device_activate_schedule_ip_config_timeout(device, AF_INET6);
+        else if (nm_device_activate_ip4_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else if (nm_device_activate_ip6_state_done(device)) {
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        } else {
+            _LOGW(LOGD_MB,
+                  "PPP failure in unexpected state %u",
+                  (guint) nm_device_get_state(device));
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+modem_prepare_result(NMModem *modem, gboolean success, guint i_reason, gpointer user_data)
+{
+    NMDeviceModem *       self   = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv   = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMDevice *            device = NM_DEVICE(self);
+    NMDeviceStateReason   reason = i_reason;
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE
+        || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING) {
+        nm_assert_not_reached();
+        success = FALSE;
+    }
+
+    if (!success) {
+        /* There are several reasons to block autoconnection at device level:
+         *
+         *  - Wrong SIM-PIN: The device won't autoconnect because it doesn't make sense
+         *    to retry the connection with the same PIN. This error also makes autoconnection
+         *    blocked at settings level, so not even a modem unplug and replug will allow
+         *    autoconnection again. It is somewhat redundant to block autoconnection at
+         *    both device and setting level really.
+         *
+         *  - SIM wrong or not inserted: If the modem is reporting a SIM not inserted error,
+         *    we can block autoconnection at device level, so that if the same device is
+         *    unplugged and replugged with a SIM (or if a SIM hotplug event happens in MM,
+         *    recreating the device completely), we can try the autoconnection again.
+         *
+         *  - Modem initialization failed: For some reason unknown to NM, the modem wasn't
+         *    initialized correctly, which leads to an unusable device. A device unplug and
+         *    replug may solve the issue, so make it a device-level autoconnection blocking
+         *    reason.
+         */
+        switch (nm_device_state_reason_check(reason)) {
+        case NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED:
+        case NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED:
+        case NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_WRONG_PIN);
+            break;
+        case NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED:
+        case NM_DEVICE_STATE_REASON_GSM_SIM_WRONG:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_SIM_MISSING);
+            break;
+        case NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED:
+            nm_device_autoconnect_blocked_set(device, NM_DEVICE_AUTOCONNECT_BLOCKED_INIT_FAILED);
+            break;
+        default:
+            break;
+        }
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, reason);
+        return;
+    }
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_COMPLETED;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_auth_requested(NMModem *modem, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    /* Auth requests (PIN, PAP/CHAP passwords, etc) only get handled
+     * during activation.
+     */
+    if (!nm_device_is_activating(device))
+        return;
+
+    nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_NONE);
+}
+
+static void
+modem_auth_result(NMModem *modem, GError *error, gpointer user_data)
+{
+    NMDevice *            device = NM_DEVICE(user_data);
+    NMDeviceModemPrivate *priv   = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    g_return_if_fail(nm_device_get_state(device) == NM_DEVICE_STATE_NEED_AUTH);
+
+    if (error) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_NO_SECRETS);
+        return;
+    }
+
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+    nm_device_activate_schedule_stage1_device_prepare(device, FALSE);
+}
+
+static void
+modem_ip4_config_result(NMModem *modem, NMIP4Config *config, GError *error, gpointer user_data)
+{
+    NMDeviceModem *self   = NM_DEVICE_MODEM(user_data);
+    NMDevice *     device = NM_DEVICE(self);
+
+    g_return_if_fail(nm_device_activate_ip4_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP4, "retrieving IPv4 configuration failed: %s", error->message);
+        nm_device_ip_method_failed(device, AF_INET, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    } else {
+        nm_device_set_dev2_ip_config(device, AF_INET, NM_IP_CONFIG_CAST(config));
+        nm_device_activate_schedule_ip_config_result(device, AF_INET, NULL);
+    }
+}
+
+static void
+modem_ip6_config_result(NMModem *    modem,
+                        NMIP6Config *config,
+                        gboolean     do_slaac,
+                        GError *     error,
+                        gpointer     user_data)
+{
+    NMDeviceModem *     self   = NM_DEVICE_MODEM(user_data);
+    NMDevice *          device = NM_DEVICE(self);
+    NMActStageReturn    ret;
+    NMDeviceStateReason failure_reason      = NM_DEVICE_STATE_REASON_NONE;
+    gs_unref_object NMIP6Config *ignored    = NULL;
+    gboolean                     got_config = !!config;
+
+    g_return_if_fail(nm_device_activate_ip6_state_in_conf(device) == TRUE);
+
+    if (error) {
+        _LOGW(LOGD_MB | LOGD_IP6, "retrieving IPv6 configuration failed: %s", error->message);
+        nm_device_ip_method_failed(device, AF_INET6, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    /* Re-enable IPv6 on the interface */
+    nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "0");
+
+    if (config)
+        nm_device_set_dev2_ip_config(device, AF_INET6, NM_IP_CONFIG_CAST(config));
+
+    if (do_slaac == FALSE) {
+        if (got_config)
+            nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL);
+        else {
+            _LOGW(LOGD_MB | LOGD_IP6,
+                  "retrieving IPv6 configuration failed: SLAAC not requested and no addresses");
+            nm_device_ip_method_failed(device,
+                                       AF_INET6,
+                                       NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        }
+        return;
+    }
+
+    /* Start SLAAC now that we have a link-local address from the modem */
+    ret =
+        NM_DEVICE_CLASS(nm_device_modem_parent_class)
+            ->act_stage3_ip_config_start(device, AF_INET6, (gpointer *) &ignored, &failure_reason);
+
+    nm_assert(ignored == NULL);
+
+    switch (ret) {
+    case NM_ACT_STAGE_RETURN_FAILURE:
+        nm_device_ip_method_failed(device, AF_INET6, failure_reason);
+        break;
+    case NM_ACT_STAGE_RETURN_IP_FAIL:
+        /* all done */
+        nm_device_activate_schedule_ip_config_result(device, AF_INET6, NULL);
+        break;
+    case NM_ACT_STAGE_RETURN_POSTPONE:
+        /* let SLAAC run */
+        break;
+    default:
+        /* Should never get here since we've assured that the IPv6 method
+         * will either be "auto" or "ignored" when starting IPv6 configuration.
+         */
+        nm_assert_not_reached();
+    }
+}
+
+static void
+ip_ifindex_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDevice *device = NM_DEVICE(user_data);
+
+    if (!nm_device_is_activating(device))
+        return;
+
+    if (!nm_device_set_ip_ifindex(device, nm_modem_get_ip_ifindex(modem))) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return;
+    }
+
+    /* Disable IPv6 immediately on the interface since NM handles IPv6
+     * internally, and leaving it enabled could allow the kernel's IPv6
+     * RA handling code to run before NM is ready.
+     */
+    nm_device_sysctl_ip_conf_set(device, AF_INET6, "disable_ipv6", "1");
+}
+
+static void
+operator_code_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDeviceModem *       self          = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv          = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    const char *          operator_code = nm_modem_get_operator_code(modem);
+
+    if (g_strcmp0(priv->operator_code, operator_code) != 0) {
+        g_free(priv->operator_code);
+        priv->operator_code = g_strdup(operator_code);
+        _notify(self, PROP_OPERATOR_CODE);
+    }
+}
+
+static void
+apn_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(user_data);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    const char *          apn  = nm_modem_get_apn(modem);
+
+    if (g_strcmp0(priv->apn, apn) != 0) {
+        g_free(priv->apn);
+        priv->apn = g_strdup(apn);
+        _notify(self, PROP_APN);
+    }
+}
+
+static void
+ids_changed_cb(NMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    nm_device_recheck_available_connections(NM_DEVICE(user_data));
+}
+
+static void
+modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_data)
+{
+    NMModemState          new_state = new_state_i;
+    NMModemState          old_state = old_state_i;
+    NMDevice *            device    = NM_DEVICE(user_data);
+    NMDeviceModemPrivate *priv      = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMDeviceState         dev_state = nm_device_get_state(device);
+
+    if (new_state <= NM_MODEM_STATE_DISABLING && old_state > NM_MODEM_STATE_DISABLING
+        && priv->rf_enabled) {
+        /* Called when the ModemManager modem enabled state is changed externally
+         * to NetworkManager (eg something using MM's D-Bus API directly).
+         */
+        if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
+            /* user-initiated action, hence DISCONNECTED not FAILED */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DISCONNECTED,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+    }
+
+    if (new_state < NM_MODEM_STATE_CONNECTING && old_state >= NM_MODEM_STATE_CONNECTING
+        && dev_state >= NM_DEVICE_STATE_NEED_AUTH && dev_state <= NM_DEVICE_STATE_ACTIVATED) {
+        /* Fail the device if the modem disconnects unexpectedly while the
+         * device is activating/activated. */
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+
+    if (new_state > NM_MODEM_STATE_LOCKED && old_state == NM_MODEM_STATE_LOCKED) {
+        /* If the modem is now unlocked, enable/disable it according to the
+         * device's enabled/disabled state.
+         */
+        nm_modem_set_mm_enabled(priv->modem, priv->rf_enabled);
+
+        if (dev_state == NM_DEVICE_STATE_NEED_AUTH) {
+            /* The modem was unlocked externally to NetworkManager,
+             * deactivate so the default connection can be
+             * automatically activated again */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_DEACTIVATING,
+                                    NM_DEVICE_STATE_REASON_MODEM_AVAILABLE);
+        }
+
+        /* Now allow connections without a PIN to be available */
+        nm_device_recheck_available_connections(device);
+    }
+
+    nm_device_queue_recheck_available(device,
+                                      NM_DEVICE_STATE_REASON_MODEM_AVAILABLE,
+                                      NM_DEVICE_STATE_REASON_MODEM_FAILED);
+}
+
+static void
+modem_removed_cb(NMModem *modem, gpointer user_data)
+{
+    g_signal_emit_by_name(NM_DEVICE(user_data), NM_DEVICE_REMOVED);
+}
+
+/*****************************************************************************/
+
+static gboolean
+owns_iface(NMDevice *device, const char *iface)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    g_return_val_if_fail(priv->modem, FALSE);
+
+    return nm_modem_owns_port(priv->modem, iface);
+}
+
+/*****************************************************************************/
+
+static void
+device_state_changed(NMDevice *          device,
+                     NMDeviceState       new_state,
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->modem);
+
+    if (new_state == NM_DEVICE_STATE_UNAVAILABLE && old_state < NM_DEVICE_STATE_UNAVAILABLE) {
+        /* Log initial modem state */
+        _LOGI(LOGD_MB,
+              "modem state '%s'",
+              nm_modem_state_to_string(nm_modem_get_state(priv->modem)));
+    }
+    nm_modem_device_state_changed(priv->modem, new_state, old_state);
+}
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *device)
+{
+    return NM_DEVICE_CAP_IS_NON_KERNEL;
+}
+
+static const char *
+get_type_description(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS))
+        return "gsm";
+    if (NM_FLAGS_HAS(priv->current_caps, NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO))
+        return "cdma";
+    return NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_type_description(device);
+}
+
+static gboolean
+check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+{
+    GError *local = NULL;
+
+    if (!NM_DEVICE_CLASS(nm_device_modem_parent_class)
+             ->check_connection_compatible(device, connection, error))
+        return FALSE;
+
+    if (!nm_modem_check_connection_compatible(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem,
+                                              connection,
+                                              error ? &local : NULL)) {
+        if (error) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        g_error_matches(local,
+                                        NM_UTILS_ERROR,
+                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE)
+                            ? NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE
+                            : NM_UTILS_ERROR_UNKNOWN,
+                        "modem is incompatible with connection: %s",
+                        local->message);
+            g_error_free(local);
+        }
+        return FALSE;
+    }
+    return TRUE;
+}
+
+static gboolean
+check_connection_available(NMDevice *                     device,
+                           NMConnection *                 connection,
+                           NMDeviceCheckConAvailableFlags flags,
+                           const char *                   specific_object,
+                           GError **                      error)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMModemState          state;
+
+    if (!priv->rf_enabled) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "RFKILL for modem enabled");
+        return FALSE;
+    }
+
+    if (!priv->modem) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem not available");
+        return FALSE;
+    }
+
+    state = nm_modem_get_state(priv->modem);
+    if (state <= NM_MODEM_STATE_INITIALIZING) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem not initialized");
+        return FALSE;
+    }
+
+    if (state == NM_MODEM_STATE_LOCKED) {
+        if (!nm_connection_get_setting_gsm(connection)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                       "modem is locked without pin available");
+            return FALSE;
+        }
+    }
+
+    return TRUE;
+}
+
+static gboolean
+complete_connection(NMDevice *           device,
+                    NMConnection *       connection,
+                    const char *         specific_object,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    return nm_modem_complete_connection(priv->modem,
+                                        nm_device_get_iface(device),
+                                        connection,
+                                        existing_connections,
+                                        error);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    nm_modem_deactivate(priv->modem, device);
+    priv->stage1_state = NM_DEVICE_STAGE_STATE_INIT;
+}
+
+/*****************************************************************************/
+
+static void
+modem_deactivate_async_cb(NMModem *modem, GError *error, gpointer user_data)
+{
+    gs_unref_object NMDevice * self = NULL;
+    NMDeviceDeactivateCallback callback;
+    gpointer                   callback_user_data;
+
+    nm_utils_user_data_unpack(user_data, &self, &callback, &callback_user_data);
+    callback(self, error, callback_user_data);
+}
+
+static void
+deactivate_async(NMDevice *                 self,
+                 GCancellable *             cancellable,
+                 NMDeviceDeactivateCallback callback,
+                 gpointer                   user_data)
+{
+    nm_assert(G_IS_CANCELLABLE(cancellable));
+    nm_assert(callback);
+
+    nm_modem_deactivate_async(NM_DEVICE_MODEM_GET_PRIVATE(self)->modem,
+                              self,
+                              cancellable,
+                              modem_deactivate_async_cb,
+                              nm_utils_user_data_pack(g_object_ref(self), callback, user_data));
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMActRequest *        req;
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_INIT) {
+        priv->stage1_state = NM_DEVICE_STAGE_STATE_PENDING;
+        return nm_modem_act_stage1_prepare(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem,
+                                           req,
+                                           out_failure_reason);
+    }
+
+    if (priv->stage1_state == NM_DEVICE_STAGE_STATE_PENDING)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    nm_assert(priv->stage1_state == NM_DEVICE_STAGE_STATE_COMPLETED);
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
+{
+    nm_modem_act_stage2_config(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem);
+    return NM_ACT_STAGE_RETURN_SUCCESS;
+}
+
+static NMActStageReturn
+act_stage3_ip_config_start(NMDevice *           device,
+                           int                  addr_family,
+                           gpointer *           out_config,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(device);
+
+    nm_assert_addr_family(addr_family);
+
+    if (addr_family == AF_INET) {
+        return nm_modem_stage3_ip4_config_start(priv->modem,
+                                                device,
+                                                NM_DEVICE_CLASS(nm_device_modem_parent_class),
+                                                out_failure_reason);
+    } else {
+        return nm_modem_stage3_ip6_config_start(priv->modem, device, out_failure_reason);
+    }
+}
+
+static void
+ip4_config_pre_commit(NMDevice *device, NMIP4Config *config)
+{
+    nm_modem_ip4_pre_commit(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem, device, config);
+}
+
+static gboolean
+get_ip_iface_identifier(NMDevice *device, NMUtilsIPv6IfaceId *out_iid)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    gboolean              success;
+
+    g_return_val_if_fail(priv->modem, FALSE);
+    success = nm_modem_get_iid(priv->modem, out_iid);
+    if (!success)
+        success =
+            NM_DEVICE_CLASS(nm_device_modem_parent_class)->get_ip_iface_identifier(device, out_iid);
+    return success;
+}
+
+/*****************************************************************************/
+
+static gboolean
+get_enabled(NMDevice *device)
+{
+    NMDeviceModemPrivate *priv        = NM_DEVICE_MODEM_GET_PRIVATE(device);
+    NMModemState          modem_state = nm_modem_get_state(priv->modem);
+
+    return priv->rf_enabled && (modem_state >= NM_MODEM_STATE_LOCKED);
+}
+
+static void
+set_enabled(NMDevice *device, gboolean enabled)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    /* Called only by the Manager in response to rfkill switch changes or
+     * global user WWAN enable/disable preference changes.
+     */
+    priv->rf_enabled = enabled;
+
+    if (priv->modem) {
+        /* Sync the ModemManager modem enabled/disabled with rfkill/user preference */
+        nm_modem_set_mm_enabled(priv->modem, enabled);
+    }
+
+    if (enabled == FALSE) {
+        nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE);
+    }
+}
+
+static gboolean
+is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
+{
+    NMDeviceModem *       self = NM_DEVICE_MODEM(device);
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+    NMModemState          modem_state;
+
+    if (!priv->rf_enabled)
+        return FALSE;
+
+    g_assert(priv->modem);
+    modem_state = nm_modem_get_state(priv->modem);
+    if (modem_state <= NM_MODEM_STATE_INITIALIZING)
+        return FALSE;
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+set_modem(NMDeviceModem *self, NMModem *modem)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(modem != NULL);
+
+    priv->modem = nm_modem_claim(modem);
+
+    g_signal_connect(modem, NM_MODEM_PPP_FAILED, G_CALLBACK(ppp_failed), self);
+    g_signal_connect(modem, NM_MODEM_PREPARE_RESULT, G_CALLBACK(modem_prepare_result), self);
+    g_signal_connect(modem, NM_MODEM_IP4_CONFIG_RESULT, G_CALLBACK(modem_ip4_config_result), self);
+    g_signal_connect(modem, NM_MODEM_IP6_CONFIG_RESULT, G_CALLBACK(modem_ip6_config_result), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_REQUESTED, G_CALLBACK(modem_auth_requested), self);
+    g_signal_connect(modem, NM_MODEM_AUTH_RESULT, G_CALLBACK(modem_auth_result), self);
+    g_signal_connect(modem, NM_MODEM_STATE_CHANGED, G_CALLBACK(modem_state_cb), self);
+    g_signal_connect(modem, NM_MODEM_REMOVED, G_CALLBACK(modem_removed_cb), self);
+
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_IP_IFINDEX,
+                     G_CALLBACK(ip_ifindex_changed_cb),
+                     self);
+    g_signal_connect(modem, "notify::" NM_MODEM_DEVICE_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem, "notify::" NM_MODEM_SIM_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem, "notify::" NM_MODEM_SIM_OPERATOR_ID, G_CALLBACK(ids_changed_cb), self);
+    g_signal_connect(modem,
+                     "notify::" NM_MODEM_OPERATOR_CODE,
+                     G_CALLBACK(operator_code_changed_cb),
+                     self);
+    g_signal_connect(modem, "notify::" NM_MODEM_APN, G_CALLBACK(apn_changed_cb), self);
+}
+
+static guint32
+get_dhcp_timeout_for_device(NMDevice *device, int addr_family)
+{
+    /* DHCP is always done by the modem firmware, not by the network, and
+     * by the time we get around to DHCP the firmware should already know
+     * the IP addressing details.  So the DHCP timeout can be much shorter.
+     */
+    return 15;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        g_value_set_object(value, priv->modem);
+        break;
+    case PROP_CAPABILITIES:
+        g_value_set_uint(value, priv->caps);
+        break;
+    case PROP_CURRENT_CAPABILITIES:
+        g_value_set_uint(value, priv->current_caps);
+        break;
+    case PROP_DEVICE_ID:
+        g_value_set_string(value, priv->device_id);
+        break;
+    case PROP_OPERATOR_CODE:
+        g_value_set_string(value, priv->operator_code);
+        break;
+    case PROP_APN:
+        g_value_set_string(value, priv->apn);
+        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)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        /* construct-only */
+        set_modem(NM_DEVICE_MODEM(object), g_value_get_object(value));
+        break;
+    case PROP_CAPABILITIES:
+        priv->caps = g_value_get_uint(value);
+        break;
+    case PROP_CURRENT_CAPABILITIES:
+        priv->current_caps = g_value_get_uint(value);
+        break;
+    case PROP_DEVICE_ID:
+        /* construct-only */
+        priv->device_id = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_device_modem_init(NMDeviceModem *self)
+{}
+
+NMDevice *
+nm_device_modem_new(NMModem *modem)
+{
+    NMDeviceModemCapabilities caps         = NM_DEVICE_MODEM_CAPABILITY_NONE;
+    NMDeviceModemCapabilities current_caps = NM_DEVICE_MODEM_CAPABILITY_NONE;
+
+    g_return_val_if_fail(NM_IS_MODEM(modem), NULL);
+
+    /* Load capabilities */
+    nm_modem_get_capabilities(modem, &caps, &current_caps);
+
+    return g_object_new(NM_TYPE_DEVICE_MODEM,
+                        NM_DEVICE_UDI,
+                        nm_modem_get_path(modem),
+                        NM_DEVICE_IFACE,
+                        nm_modem_get_uid(modem),
+                        NM_DEVICE_DRIVER,
+                        nm_modem_get_driver(modem),
+                        NM_DEVICE_TYPE_DESC,
+                        "Broadband",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_MODEM,
+                        NM_DEVICE_RFKILL_TYPE,
+                        RFKILL_TYPE_WWAN,
+                        NM_DEVICE_MODEM_MODEM,
+                        modem,
+                        NM_DEVICE_MODEM_CAPABILITIES,
+                        caps,
+                        NM_DEVICE_MODEM_CURRENT_CAPABILITIES,
+                        current_caps,
+                        NM_DEVICE_MODEM_DEVICE_ID,
+                        nm_modem_get_device_id(modem),
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMDeviceModemPrivate *priv = NM_DEVICE_MODEM_GET_PRIVATE(object);
+
+    if (priv->modem) {
+        g_signal_handlers_disconnect_by_data(priv->modem, NM_DEVICE_MODEM(object));
+        nm_clear_pointer(&priv->modem, nm_modem_unclaim);
+    }
+
+    nm_clear_g_free(&priv->device_id);
+    nm_clear_g_free(&priv->operator_code);
+    nm_clear_g_free(&priv->apn);
+
+    G_OBJECT_CLASS(nm_device_modem_parent_class)->dispose(object);
+}
+
+static const NMDBusInterfaceInfoExtended interface_info_device_modem = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_MODEM,
+        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ),
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("ModemCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_MODEM_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("CurrentCapabilities",
+                                                             "u",
+                                                             NM_DEVICE_MODEM_CURRENT_CAPABILITIES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DeviceId",
+                                                           "s",
+                                                           NM_DEVICE_MODEM_DEVICE_ID),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("OperatorCode",
+                                                           "s",
+                                                           NM_DEVICE_MODEM_OPERATOR_CODE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Apn", "s", NM_DEVICE_MODEM_APN), ), ),
+    .legacy_property_changed = TRUE,
+};
+
+static void
+nm_device_modem_class_init(NMDeviceModemClass *klass)
+{
+    GObjectClass *     object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass *    device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_modem);
+
+    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->check_connection_available  = check_connection_available;
+    device_class->complete_connection         = complete_connection;
+    device_class->deactivate_async            = deactivate_async;
+    device_class->deactivate                  = deactivate;
+    device_class->act_stage1_prepare          = act_stage1_prepare;
+    device_class->act_stage2_config           = act_stage2_config;
+    device_class->act_stage3_ip_config_start  = act_stage3_ip_config_start;
+    device_class->ip4_config_pre_commit       = ip4_config_pre_commit;
+    device_class->get_enabled                 = get_enabled;
+    device_class->set_enabled                 = set_enabled;
+    device_class->owns_iface                  = owns_iface;
+    device_class->is_available                = is_available;
+    device_class->get_ip_iface_identifier     = get_ip_iface_identifier;
+    device_class->get_configured_mtu          = nm_modem_get_configured_mtu;
+    device_class->get_dhcp_timeout_for_device = get_dhcp_timeout_for_device;
+
+    device_class->state_changed = device_state_changed;
+
+    obj_properties[PROP_MODEM] =
+        g_param_spec_object(NM_DEVICE_MODEM_MODEM,
+                            "",
+                            "",
+                            NM_TYPE_MODEM,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_MODEM_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_MODEM_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CURRENT_CAPABILITIES] =
+        g_param_spec_uint(NM_DEVICE_MODEM_CURRENT_CAPABILITIES,
+                          "",
+                          "",
+                          0,
+                          G_MAXUINT32,
+                          NM_DEVICE_MODEM_CAPABILITY_NONE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DEVICE_ID] =
+        g_param_spec_string(NM_DEVICE_MODEM_DEVICE_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_OPERATOR_CODE] =
+        g_param_spec_string(NM_DEVICE_MODEM_OPERATOR_CODE,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_APN] = g_param_spec_string(NM_DEVICE_MODEM_APN,
+                                                   "",
+                                                   "",
+                                                   NULL,
+                                                   G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wwan/nm-device-modem.h b/src/core/devices/wwan/nm-device-modem.h
new file mode 100644
index 00000000..f171d76f
--- /dev/null
+++ b/src/core/devices/wwan/nm-device-modem.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2011 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_MODEM_H__
+#define __NETWORKMANAGER_DEVICE_MODEM_H__
+
+#include "devices/nm-device.h"
+#include "nm-modem.h"
+
+#define NM_TYPE_DEVICE_MODEM (nm_device_modem_get_type())
+#define NM_DEVICE_MODEM(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModem))
+#define NM_DEVICE_MODEM_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass))
+#define NM_IS_DEVICE_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_MODEM))
+#define NM_IS_DEVICE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_MODEM))
+#define NM_DEVICE_MODEM_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_MODEM, NMDeviceModemClass))
+
+#define NM_DEVICE_MODEM_MODEM                "modem"
+#define NM_DEVICE_MODEM_CAPABILITIES         "modem-capabilities"
+#define NM_DEVICE_MODEM_CURRENT_CAPABILITIES "current-capabilities"
+#define NM_DEVICE_MODEM_DEVICE_ID            "device-id"
+#define NM_DEVICE_MODEM_OPERATOR_CODE        "operator-code"
+#define NM_DEVICE_MODEM_APN                  "apn"
+
+typedef struct _NMDeviceModem      NMDeviceModem;
+typedef struct _NMDeviceModemClass NMDeviceModemClass;
+
+GType nm_device_modem_get_type(void);
+
+NMDevice *nm_device_modem_new(NMModem *modem);
+
+#endif /* __NETWORKMANAGER_DEVICE_MODEM_H__ */
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
new file mode 100644
index 00000000..ca028804
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -0,0 +1,1625 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 Aleksander Morgado <aleksander@gnu.org>
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-broadband.h"
+#include "nm-service-providers.h"
+
+#include <arpa/inet.h>
+#include <libmm-glib.h>
+
+#include "nm-core-internal.h"
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "platform/nm-platform.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+
+#define NM_MODEM_BROADBAND_MODEM "modem"
+
+static gboolean
+MODEM_CAPS_3GPP(MMModemCapability caps)
+{
+    G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+    /* MM_MODEM_CAPABILITY_LTE_ADVANCED is marked as deprecated since ModemManager 1.14.0.
+     *
+     * The flag probably was never used, it certainly isn't used since 1.14.0.
+     *
+     * Still, just to be sure, there is no harm in checking it here. Suppress the
+     * warning, it should have no bad effect.
+     */
+    return NM_FLAGS_ANY(caps,
+                        (MM_MODEM_CAPABILITY_GSM_UMTS | MM_MODEM_CAPABILITY_LTE
+                         | MM_MODEM_CAPABILITY_LTE_ADVANCED));
+    G_GNUC_END_IGNORE_DEPRECATIONS
+}
+
+#define MODEM_CAPS_3GPP2(caps) (caps & (MM_MODEM_CAPABILITY_CDMA_EVDO))
+
+/* Maximum time to keep the DBus call waiting for a connection result.
+ * This value is greater than the default timeout in ModemManager (180s since
+ * 1.16), so that whenever possible the timeout happens first there instead of
+ * in NetworkManager. */
+#define MODEM_CONNECT_TIMEOUT_SECS 200
+
+/*****************************************************************************/
+
+typedef enum {
+    CONNECT_STEP_FIRST,
+    CONNECT_STEP_WAIT_FOR_SIM,
+    CONNECT_STEP_UNLOCK,
+    CONNECT_STEP_WAIT_FOR_READY,
+    CONNECT_STEP_CONNECT,
+    CONNECT_STEP_LAST,
+} ConnectStep;
+
+typedef struct {
+    NMModemBroadband *self;
+    ConnectStep       step;
+
+    MMModemCapability          caps;
+    NMConnection *             connection;
+    GCancellable *             cancellable;
+    MMSimpleConnectProperties *connect_properties;
+    GArray *                   ip_types;
+    guint                      ip_types_i;
+    guint                      ip_type_tries;
+    GError *                   first_error;
+} ConnectContext;
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_MODEM, );
+
+typedef struct {
+    /* The modem object from dbus */
+    MMObject *modem_object;
+    /* Per-interface objects */
+    MMModem *      modem_iface;
+    MMModem3gpp *  modem_3gpp_iface;
+    MMModemSimple *simple_iface;
+    MMSim *        sim_iface;
+
+    /* Connection setup */
+    ConnectContext *ctx;
+
+    MMBearer *        bearer;
+    MMBearerIpConfig *ipv4_config;
+    MMBearerIpConfig *ipv6_config;
+
+    guint idle_id_ip4;
+    guint idle_id_ip6;
+
+    guint32 pin_tries;
+} NMModemBroadbandPrivate;
+
+struct _NMModemBroadband {
+    NMModem                 parent;
+    NMModemBroadbandPrivate _priv;
+};
+
+struct _NMModemBroadbandClass {
+    NMModemClass parent;
+};
+
+G_DEFINE_TYPE(NMModemBroadband, nm_modem_broadband, NM_TYPE_MODEM)
+
+#define NM_MODEM_BROADBAND_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMModemBroadband, NM_IS_MODEM_BROADBAND)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG_PREFIX_NAME "modem-broadband"
+#define _NMLOG(level, ...)                                                       \
+    G_STMT_START                                                                 \
+    {                                                                            \
+        const NMLogLevel _level = (level);                                       \
+                                                                                 \
+        if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) {                       \
+            NMModemBroadband *const __self = (self);                             \
+            char                    __prefix_name[128];                          \
+            const char *            __uid;                                       \
+                                                                                 \
+            _nm_log(_level,                                                      \
+                    (_NMLOG_DOMAIN),                                             \
+                    0,                                                           \
+                    NULL,                                                        \
+                    ((__self && __self->_priv.ctx)                               \
+                         ? nm_connection_get_uuid(__self->_priv.ctx->connection) \
+                         : NULL),                                                \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                 \
+                    _NMLOG_PREFIX_NAME,                                          \
+                    (__self ? ({                                                 \
+                        ((__uid = nm_modem_get_uid((NMModem *) __self))          \
+                             ? nm_sprintf_buf(__prefix_name, "[%s]", __uid)      \
+                             : "(null)");                                        \
+                    })                                                           \
+                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));            \
+        }                                                                        \
+    }                                                                            \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static NMDeviceStateReason
+translate_mm_error(NMModemBroadband *self, GError *error)
+{
+    NMDeviceStateReason reason;
+
+    g_return_val_if_fail(error != NULL, NM_DEVICE_STATE_REASON_UNKNOWN);
+
+    if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_CARRIER))
+        reason = NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_DIALTONE))
+        reason = NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_BUSY))
+        reason = NM_DEVICE_STATE_REASON_MODEM_BUSY;
+    else if (g_error_matches(error, MM_CONNECTION_ERROR, MM_CONNECTION_ERROR_NO_ANSWER))
+        reason = NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NETWORK_NOT_ALLOWED))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NETWORK_TIMEOUT))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_NO_NETWORK))
+        reason = NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_SIM_NOT_INSERTED))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_NOT_INSERTED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_PIN_REQUIRED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PUK))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_PUK_REQUIRED;
+    else if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_WRONG))
+        reason = NM_DEVICE_STATE_REASON_GSM_SIM_WRONG;
+    else if (g_error_matches(error,
+                             MM_MOBILE_EQUIPMENT_ERROR,
+                             MM_MOBILE_EQUIPMENT_ERROR_INCORRECT_PASSWORD))
+        reason = NM_DEVICE_STATE_REASON_SIM_PIN_INCORRECT;
+    else {
+        /* unable to map the ModemManager error to a NM_DEVICE_STATE_REASON */
+        _LOGD("unmapped error detected: '%s'", error->message);
+        reason = NM_DEVICE_STATE_REASON_UNKNOWN;
+    }
+
+    return reason;
+}
+
+/*****************************************************************************/
+
+static void
+get_capabilities(NMModem *                  _self,
+                 NMDeviceModemCapabilities *modem_caps,
+                 NMDeviceModemCapabilities *current_caps)
+{
+    NMModemBroadband * self          = NM_MODEM_BROADBAND(_self);
+    MMModemCapability  all_supported = MM_MODEM_CAPABILITY_NONE;
+    MMModemCapability *supported;
+    guint              n_supported;
+
+    /* For now, we don't care about the capability combinations, just merge all
+     * combinations in a single mask */
+    if (mm_modem_get_supported_capabilities(self->_priv.modem_iface, &supported, &n_supported)) {
+        guint i;
+
+        for (i = 0; i < n_supported; i++)
+            all_supported |= supported[i];
+
+        g_free(supported);
+    }
+
+    *modem_caps = (NMDeviceModemCapabilities) all_supported;
+    *current_caps =
+        (NMDeviceModemCapabilities) mm_modem_get_current_capabilities(self->_priv.modem_iface);
+}
+
+static gboolean
+owns_port(NMModem *_self, const char *iface)
+{
+    NMModemBroadband *     self    = NM_MODEM_BROADBAND(_self);
+    const MMModemPortInfo *ports   = NULL;
+    guint                  n_ports = 0, i;
+
+    mm_modem_peek_ports(self->_priv.modem_iface, &ports, &n_ports);
+    for (i = 0; i < n_ports; i++) {
+        if (nm_streq0(iface, ports[i].name))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static void
+ask_for_pin(NMModemBroadband *self)
+{
+    guint32 tries;
+
+    tries = self->_priv.pin_tries++;
+    nm_modem_get_secrets(NM_MODEM(self),
+                         NM_SETTING_GSM_SETTING_NAME,
+                         tries ? TRUE : FALSE,
+                         NM_SETTING_GSM_PIN);
+}
+
+static NMModemIPMethod
+get_bearer_ip_method(MMBearerIpConfig *config)
+{
+    MMBearerIpMethod mm_method;
+
+    mm_method = mm_bearer_ip_config_get_method(config);
+    if (mm_method == MM_BEARER_IP_METHOD_PPP)
+        return NM_MODEM_IP_METHOD_PPP;
+    else if (mm_method == MM_BEARER_IP_METHOD_STATIC)
+        return NM_MODEM_IP_METHOD_STATIC;
+    else if (mm_method == MM_BEARER_IP_METHOD_DHCP)
+        return NM_MODEM_IP_METHOD_AUTO;
+    return NM_MODEM_IP_METHOD_UNKNOWN;
+}
+
+static MMSimpleConnectProperties *
+create_cdma_connect_properties(NMConnection *connection)
+{
+    MMSimpleConnectProperties *properties;
+
+    properties = mm_simple_connect_properties_new();
+
+#if !MM_CHECK_VERSION(1, 9, 1)
+    {
+        NMSettingCdma *setting;
+        const char *   str;
+
+        setting = nm_connection_get_setting_cdma(connection);
+        str     = nm_setting_cdma_get_number(setting);
+        if (str)
+            mm_simple_connect_properties_set_number(properties, str);
+    }
+#endif
+
+    return properties;
+}
+
+static MMSimpleConnectProperties *
+create_gsm_connect_properties(NMConnection *connection,
+                              const char *  apn,
+                              const char *  username,
+                              const char *  password)
+{
+    NMSettingGsm *             setting;
+    NMSettingPpp *             s_ppp;
+    MMSimpleConnectProperties *properties;
+    const char *               str;
+
+    setting = nm_connection_get_setting_gsm(connection);
+
+    properties = mm_simple_connect_properties_new();
+
+    mm_simple_connect_properties_set_apn(properties, apn ?: "");
+    if (username)
+        mm_simple_connect_properties_set_user(properties, username);
+    if (password)
+        mm_simple_connect_properties_set_password(properties, password);
+
+    str = nm_setting_gsm_get_network_id(setting);
+    if (str)
+        mm_simple_connect_properties_set_operator_id(properties, str);
+
+    str = nm_setting_gsm_get_pin(setting);
+    if (str)
+        mm_simple_connect_properties_set_pin(properties, str);
+
+    /* Roaming */
+    if (nm_setting_gsm_get_home_only(setting))
+        mm_simple_connect_properties_set_allow_roaming(properties, FALSE);
+
+    /* For IpMethod == STATIC or DHCP */
+    s_ppp = nm_connection_get_setting_ppp(connection);
+    if (s_ppp) {
+        MMBearerAllowedAuth allowed_auth = MM_BEARER_ALLOWED_AUTH_UNKNOWN;
+
+        if (nm_setting_ppp_get_noauth(s_ppp))
+            allowed_auth = MM_BEARER_ALLOWED_AUTH_NONE;
+        if (!nm_setting_ppp_get_refuse_pap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_PAP;
+        if (!nm_setting_ppp_get_refuse_chap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_CHAP;
+        if (!nm_setting_ppp_get_refuse_mschap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAP;
+        if (!nm_setting_ppp_get_refuse_mschapv2(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_MSCHAPV2;
+        if (!nm_setting_ppp_get_refuse_eap(s_ppp))
+            allowed_auth |= MM_BEARER_ALLOWED_AUTH_EAP;
+
+        mm_simple_connect_properties_set_allowed_auth(properties, allowed_auth);
+    }
+
+    return properties;
+}
+
+static void
+connect_context_clear(NMModemBroadband *self)
+{
+    if (self->_priv.ctx) {
+        ConnectContext *ctx = self->_priv.ctx;
+
+        g_clear_error(&ctx->first_error);
+        nm_clear_pointer(&ctx->ip_types, g_array_unref);
+        nm_clear_g_cancellable(&ctx->cancellable);
+        g_clear_object(&ctx->connection);
+        g_clear_object(&ctx->connect_properties);
+        g_clear_object(&ctx->self);
+        g_slice_free(ConnectContext, ctx);
+        self->_priv.ctx = NULL;
+    }
+}
+
+static void connect_context_step(NMModemBroadband *self);
+
+static void
+connect_ready(MMModemSimple *simple_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    ConnectContext *ctx;
+    GError *        error      = NULL;
+    NMModemIPMethod ip4_method = NM_MODEM_IP_METHOD_UNKNOWN;
+    NMModemIPMethod ip6_method = NM_MODEM_IP_METHOD_UNKNOWN;
+    MMBearer *      bearer;
+
+    bearer = mm_modem_simple_connect_finish(simple_iface, res, &error);
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        g_error_free(error);
+        return;
+    }
+
+    ctx = self->_priv.ctx;
+
+    if (!ctx)
+        return;
+
+    self->_priv.bearer = bearer;
+
+    if (!self->_priv.bearer) {
+        if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN)
+            || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED)
+                && mm_modem_get_unlock_required(self->_priv.modem_iface)
+                       == MM_MODEM_LOCK_SIM_PIN)) {
+            g_error_free(error);
+
+            /* Request PIN */
+            ask_for_pin(self);
+            connect_context_clear(self);
+            return;
+        }
+
+        /* Save the error, if it's the first one */
+        if (!ctx->first_error) {
+            /* Strip remote error info before saving it */
+            if (g_dbus_error_is_remote_error(error))
+                g_dbus_error_strip_remote_error(error);
+            ctx->first_error = error;
+        } else
+            g_clear_error(&error);
+
+        if (ctx->ip_type_tries == 0 && g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_RETRY)) {
+            /* Try one more time */
+            ctx->ip_type_tries++;
+        } else {
+            /* If the modem/provider lies and the IP type we tried isn't supported,
+             * retry with the next one, if any.
+             */
+            ctx->ip_types_i++;
+            ctx->ip_type_tries = 0;
+        }
+        connect_context_step(self);
+        return;
+    }
+
+    /* Grab IP configurations */
+    self->_priv.ipv4_config = mm_bearer_get_ipv4_config(self->_priv.bearer);
+    if (self->_priv.ipv4_config)
+        ip4_method = get_bearer_ip_method(self->_priv.ipv4_config);
+
+    self->_priv.ipv6_config = mm_bearer_get_ipv6_config(self->_priv.bearer);
+    if (self->_priv.ipv6_config)
+        ip6_method = get_bearer_ip_method(self->_priv.ipv6_config);
+
+    if (!nm_modem_set_data_port(NM_MODEM(self),
+                                NM_PLATFORM_GET,
+                                mm_bearer_get_interface(self->_priv.bearer),
+                                ip4_method,
+                                ip6_method,
+                                mm_bearer_get_ip_timeout(self->_priv.bearer),
+                                &error)) {
+        _LOGW("failed to connect modem: %s", error->message);
+        g_error_free(error);
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        connect_context_clear(self);
+        return;
+    }
+
+    ctx->step++;
+    connect_context_step(self);
+}
+
+static void
+send_pin_ready(MMSim *sim, GAsyncResult *result, NMModemBroadband *self)
+{
+    gs_free_error GError *error = NULL;
+
+    mm_sim_send_pin_finish(sim, result, &error);
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    if (!self->_priv.ctx || self->_priv.ctx->step != CONNECT_STEP_UNLOCK)
+        g_return_if_reached();
+
+    if (error) {
+        if (g_error_matches(error, MM_MOBILE_EQUIPMENT_ERROR, MM_MOBILE_EQUIPMENT_ERROR_SIM_PIN)
+            || (g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNAUTHORIZED)
+                && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN))
+            ask_for_pin(self);
+        else
+            nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, translate_mm_error(self, error));
+        return;
+    }
+
+    self->_priv.ctx->step++;
+    connect_context_step(self);
+}
+
+static void
+find_gsm_apn_cb(const char *  apn,
+                const char *  username,
+                const char *  password,
+                const char *  gateway,
+                const char *  auth_method,
+                const GSList *dns,
+                GError *      error,
+                gpointer      user_data)
+{
+    NMModemBroadband *       self = user_data;
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+    ConnectContext *         ctx  = priv->ctx;
+
+    if (error) {
+        _LOGW("failed to connect '%s': APN not found: %s",
+              nm_connection_get_id(ctx->connection),
+              error->message);
+
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
+        connect_context_clear(self);
+        return;
+    }
+
+    /* Blank APN ("") means the default subscription APN */
+    ctx->connect_properties =
+        create_gsm_connect_properties(ctx->connection, apn, username, password);
+    g_return_if_fail(ctx->connect_properties);
+    connect_context_step(self);
+}
+
+static gboolean
+try_create_connect_properties(NMModemBroadband *self)
+{
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+    ConnectContext *         ctx  = priv->ctx;
+
+    if (MODEM_CAPS_3GPP(ctx->caps)) {
+        NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+
+        if (!s_gsm || nm_setting_gsm_get_auto_config(s_gsm)) {
+            gs_unref_object MMModem3gpp *modem_3gpp = NULL;
+            const char *                 network_id = NULL;
+
+            s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+            if (s_gsm)
+                network_id = nm_setting_gsm_get_network_id(s_gsm);
+            if (!network_id) {
+                if (mm_modem_get_state(self->_priv.modem_iface) < MM_MODEM_STATE_REGISTERED)
+                    return FALSE;
+                modem_3gpp = mm_object_get_modem_3gpp(priv->modem_object);
+                network_id = mm_modem_3gpp_get_operator_code(modem_3gpp);
+            }
+            if (!network_id) {
+                _LOGW("failed to connect '%s': unable to determine the network id",
+                      nm_connection_get_id(ctx->connection));
+                goto out;
+            }
+
+            nm_service_providers_find_gsm_apn(MOBILE_BROADBAND_PROVIDER_INFO_DATABASE,
+                                              network_id,
+                                              ctx->cancellable,
+                                              find_gsm_apn_cb,
+                                              self);
+        } else {
+            ctx->connect_properties =
+                create_gsm_connect_properties(ctx->connection,
+                                              nm_setting_gsm_get_apn(s_gsm),
+                                              nm_setting_gsm_get_username(s_gsm),
+                                              nm_setting_gsm_get_password(s_gsm));
+            g_return_val_if_fail(ctx->connect_properties, TRUE);
+        }
+
+        return TRUE;
+    } else if (MODEM_CAPS_3GPP2(ctx->caps)) {
+        ctx->connect_properties = create_cdma_connect_properties(ctx->connection);
+        g_return_val_if_fail(ctx->connect_properties, FALSE);
+        return TRUE;
+    } else {
+        _LOGW("failed to connect '%s': not a mobile broadband modem",
+              nm_connection_get_id(ctx->connection));
+    }
+
+out:
+    nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+    connect_context_clear(self);
+    return TRUE;
+}
+
+static void
+connect_context_step(NMModemBroadband *self)
+{
+    ConnectContext *ctx = self->_priv.ctx;
+
+    switch (ctx->step) {
+    case CONNECT_STEP_FIRST:
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_WAIT_FOR_SIM:
+        if (MODEM_CAPS_3GPP(ctx->caps) && !self->_priv.sim_iface) {
+            /* Have to wait for the SIM to show up */
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_UNLOCK:
+        if (MODEM_CAPS_3GPP(ctx->caps)
+            && mm_modem_get_unlock_required(self->_priv.modem_iface) == MM_MODEM_LOCK_SIM_PIN) {
+            NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection);
+            const char *  pin   = nm_setting_gsm_get_pin(s_gsm);
+
+            /* If we have a PIN already, send it.  If we don't, get it. */
+            if (pin) {
+                mm_sim_send_pin(self->_priv.sim_iface,
+                                pin,
+                                ctx->cancellable,
+                                (GAsyncReadyCallback) send_pin_ready,
+                                self);
+            } else {
+                ask_for_pin(self);
+            }
+            break;
+        }
+        ctx->step++;
+        /* fall-through */
+    case CONNECT_STEP_WAIT_FOR_READY:
+    {
+        GError *error = NULL;
+
+        if (mm_modem_get_state(self->_priv.modem_iface) <= MM_MODEM_STATE_LOCKED)
+            break;
+
+        if (!try_create_connect_properties(self))
+            break;
+
+        if (!self->_priv.ctx)
+            break;
+
+        /* Build up list of IP types that we need to use in the retries */
+        ctx->ip_types = nm_modem_get_connection_ip_type(NM_MODEM(self), ctx->connection, &error);
+        if (!ctx->ip_types) {
+            _LOGW("failed to connect '%s': %s",
+                  nm_connection_get_id(ctx->connection),
+                  error->message);
+            g_clear_error(&error);
+
+            nm_modem_emit_prepare_result(NM_MODEM(self),
+                                         FALSE,
+                                         NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+            connect_context_clear(self);
+            break;
+        }
+
+        ctx->step++;
+    }
+        /* fall-through */
+    case CONNECT_STEP_CONNECT:
+        if (!ctx->connect_properties)
+            break;
+
+        if (ctx->ip_types_i < ctx->ip_types->len) {
+            NMModemIPType current;
+
+            current = g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i);
+
+            if (current == NM_MODEM_IP_TYPE_IPV4)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV4);
+            else if (current == NM_MODEM_IP_TYPE_IPV6)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV6);
+            else if (current == NM_MODEM_IP_TYPE_IPV4V6)
+                mm_simple_connect_properties_set_ip_type(ctx->connect_properties,
+                                                         MM_BEARER_IP_FAMILY_IPV4V6);
+            else
+                g_return_if_reached();
+
+            _nm_modem_set_apn(NM_MODEM(self),
+                              mm_simple_connect_properties_get_apn(ctx->connect_properties));
+
+            _LOGD("launching connection with ip type '%s' (try %d)",
+                  nm_modem_ip_type_to_string(current),
+                  ctx->ip_type_tries + 1);
+
+            mm_modem_simple_connect(self->_priv.simple_iface,
+                                    ctx->connect_properties,
+                                    ctx->cancellable,
+                                    (GAsyncReadyCallback) connect_ready,
+                                    self);
+            break;
+        }
+
+        ctx->step++;
+        /* fall-through */
+
+    case CONNECT_STEP_LAST:
+        if (self->_priv.ipv4_config || self->_priv.ipv6_config)
+            nm_modem_emit_prepare_result(NM_MODEM(self), TRUE, NM_DEVICE_STATE_REASON_NONE);
+        else {
+            /* If we have a saved error from a previous attempt, use it */
+            if (!ctx->first_error)
+                ctx->first_error = g_error_new_literal(NM_DEVICE_ERROR,
+                                                       NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                                       "invalid bearer IP configuration");
+
+            _LOGW("failed to connect modem: %s", ctx->first_error->message);
+            nm_modem_emit_prepare_result(NM_MODEM(self),
+                                         FALSE,
+                                         translate_mm_error(self, ctx->first_error));
+        }
+
+        connect_context_clear(self);
+        break;
+    }
+}
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            _self,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+
+    /* Make sure we can get the Simple interface from the modem */
+    if (!self->_priv.simple_iface) {
+        self->_priv.simple_iface = mm_object_get_modem_simple(self->_priv.modem_object);
+        if (!self->_priv.simple_iface) {
+            _LOGW("cannot access the Simple mobile broadband modem interface");
+            NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+            return NM_ACT_STAGE_RETURN_FAILURE;
+        }
+    }
+
+    connect_context_clear(self);
+
+    /* Allocate new context for this connect stage attempt */
+    self->_priv.ctx              = g_slice_new0(ConnectContext);
+    self->_priv.ctx->caps        = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+    self->_priv.ctx->cancellable = g_cancellable_new();
+    self->_priv.ctx->connection  = g_object_ref(connection);
+
+    g_dbus_proxy_set_default_timeout(G_DBUS_PROXY(self->_priv.simple_iface),
+                                     MODEM_CONNECT_TIMEOUT_SECS * 1000);
+    connect_context_step(self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+check_connection_compatible_with_modem(NMModem *_self, NMConnection *connection, GError **error)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+    MMModemCapability modem_caps;
+
+    modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+
+    if (MODEM_CAPS_3GPP(modem_caps)) {
+        if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error))
+            return FALSE;
+
+        return TRUE;
+    }
+
+    if (MODEM_CAPS_3GPP2(modem_caps)) {
+        if (!_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, error))
+            return FALSE;
+
+        return TRUE;
+    }
+
+    if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)
+        && !_nm_connection_check_main_setting(connection, NM_SETTING_CDMA_SETTING_NAME, NULL)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                           "connection type %s is not supported by modem",
+                           nm_connection_get_connection_type(connection));
+        return FALSE;
+    }
+
+    nm_utils_error_set(error,
+                       NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                       "modem lacks capabilities for %s profile",
+                       nm_connection_get_connection_type(connection));
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+complete_connection(NMModem *            modem,
+                    const char *         iface,
+                    NMConnection *       connection,
+                    NMConnection *const *existing_connections,
+                    GError **            error)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(modem);
+    MMModemCapability modem_caps;
+    NMSettingPpp *    s_ppp;
+
+    modem_caps = mm_modem_get_current_capabilities(self->_priv.modem_iface);
+
+    /* PPP settings common to 3GPP and 3GPP2 */
+    s_ppp = nm_connection_get_setting_ppp(connection);
+    if (!s_ppp) {
+        s_ppp = (NMSettingPpp *) nm_setting_ppp_new();
+        g_object_set(G_OBJECT(s_ppp),
+                     NM_SETTING_PPP_LCP_ECHO_FAILURE,
+                     5,
+                     NM_SETTING_PPP_LCP_ECHO_INTERVAL,
+                     30,
+                     NULL);
+        nm_connection_add_setting(connection, NM_SETTING(s_ppp));
+    }
+
+    if (MODEM_CAPS_3GPP(modem_caps)) {
+        NMSettingGsm *s_gsm;
+
+        s_gsm = nm_connection_get_setting_gsm(connection);
+        if (!s_gsm) {
+            s_gsm = (NMSettingGsm *) nm_setting_gsm_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_gsm));
+            g_object_set(G_OBJECT(s_gsm), NM_SETTING_GSM_AUTO_CONFIG, TRUE, NULL);
+        }
+
+        if (!nm_setting_gsm_get_device_id(s_gsm)) {
+            g_object_set(G_OBJECT(s_gsm),
+                         NM_SETTING_GSM_DEVICE_ID,
+                         nm_modem_get_device_id(modem),
+                         NULL);
+        }
+
+        nm_utils_complete_generic(NM_PLATFORM_GET,
+                                  connection,
+                                  NM_SETTING_GSM_SETTING_NAME,
+                                  existing_connections,
+                                  NULL,
+                                  _("GSM connection"),
+                                  NULL,
+                                  NULL,
+                                  FALSE); /* No IPv6 yet by default */
+
+        return TRUE;
+    }
+
+    if (MODEM_CAPS_3GPP2(modem_caps)) {
+        NMSettingCdma *s_cdma;
+
+        s_cdma = nm_connection_get_setting_cdma(connection);
+        if (!s_cdma) {
+            s_cdma = (NMSettingCdma *) nm_setting_cdma_new();
+            nm_connection_add_setting(connection, NM_SETTING(s_cdma));
+        }
+
+        if (!nm_setting_cdma_get_number(s_cdma))
+            g_object_set(G_OBJECT(s_cdma), NM_SETTING_CDMA_NUMBER, "#777", NULL);
+
+        nm_utils_complete_generic(NM_PLATFORM_GET,
+                                  connection,
+                                  NM_SETTING_CDMA_SETTING_NAME,
+                                  existing_connections,
+                                  NULL,
+                                  _("CDMA connection"),
+                                  NULL,
+                                  iface,
+                                  FALSE); /* No IPv6 yet by default */
+
+        return TRUE;
+    }
+
+    g_set_error(error,
+                NM_DEVICE_ERROR,
+                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                "Device is not a mobile broadband modem");
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+get_user_pass(NMModem *modem, NMConnection *connection, const char **user, const char **pass)
+{
+    NMSettingGsm * s_gsm;
+    NMSettingCdma *s_cdma;
+
+    s_gsm  = nm_connection_get_setting_gsm(connection);
+    s_cdma = nm_connection_get_setting_cdma(connection);
+    if (!s_gsm && !s_cdma)
+        return FALSE;
+
+    if (user) {
+        if (s_gsm)
+            *user = nm_setting_gsm_get_username(s_gsm);
+        else if (s_cdma)
+            *user = nm_setting_cdma_get_username(s_cdma);
+    }
+    if (pass) {
+        if (s_gsm)
+            *pass = nm_setting_gsm_get_password(s_gsm);
+        else if (s_cdma)
+            *pass = nm_setting_cdma_get_password(s_cdma);
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+/* Query/Update enabled state */
+
+static void
+set_power_state_low_ready(MMModem *modem, GAsyncResult *result, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (!mm_modem_set_power_state_finish(modem, result, &error)) {
+        /* Log but ignore errors; not all modems support low power state */
+        _LOGD("failed to set modem low power state: %s", NM_G_ERROR_MSG(error));
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+modem_disable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (mm_modem_disable_finish(modem_iface, res, &error)) {
+        /* Once disabled, move to low-power mode */
+        mm_modem_set_power_state(modem_iface,
+                                 MM_MODEM_POWER_STATE_LOW,
+                                 NULL,
+                                 (GAsyncReadyCallback) set_power_state_low_ready,
+                                 g_object_ref(self));
+    } else {
+        _LOGW("failed to disable modem: %s", NM_G_ERROR_MSG(error));
+        nm_modem_set_prev_state(NM_MODEM(self), "disable failed");
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+modem_enable_ready(MMModem *modem_iface, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+
+    if (!mm_modem_enable_finish(modem_iface, res, &error)) {
+        _LOGW("failed to enable modem: %s", NM_G_ERROR_MSG(error));
+        nm_modem_set_prev_state(NM_MODEM(self), "enable failed");
+        g_clear_error(&error);
+    }
+
+    /* Balance refcount */
+    g_object_unref(self);
+}
+
+static void
+set_mm_enabled(NMModem *_self, gboolean enabled)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(_self);
+
+    if (enabled) {
+        mm_modem_enable(self->_priv.modem_iface,
+                        NULL, /* cancellable */
+                        (GAsyncReadyCallback) modem_enable_ready,
+                        g_object_ref(self));
+    } else {
+        mm_modem_disable(self->_priv.modem_iface,
+                         NULL, /* cancellable */
+                         (GAsyncReadyCallback) modem_disable_ready,
+                         g_object_ref(self));
+    }
+}
+
+/*****************************************************************************/
+/* IPv4 method static */
+
+static gboolean
+static_stage3_ip4_done(NMModemBroadband *self)
+{
+    GError *        error               = NULL;
+    gs_unref_object NMIP4Config *config = NULL;
+    const char *                 data_port;
+    const char *                 address_string;
+    const char *                 gw_string;
+    guint32                      address_network;
+    guint32                      gw = 0;
+    NMPlatformIP4Address         address;
+    const char **                dns;
+    guint                        i;
+    guint32                      ip4_route_table, ip4_route_metric;
+    NMPlatformIP4Route *         r;
+    guint32                      mtu_n;
+
+    g_return_val_if_fail(self->_priv.ipv4_config, FALSE);
+    g_return_val_if_fail(self->_priv.bearer, FALSE);
+
+    self->_priv.idle_id_ip4 = 0;
+
+    _LOGI("IPv4 static configuration:");
+
+    /* Fully fail if invalid IP address retrieved */
+    address_string = mm_bearer_ip_config_get_address(self->_priv.ipv4_config);
+    if (!address_string
+        || !nm_utils_parse_inaddr_bin(AF_INET, address_string, NULL, &address_network)) {
+        error =
+            g_error_new(NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "(%s) retrieving IP4 configuration failed: invalid address given %s%s%s",
+                        nm_modem_get_uid(NM_MODEM(self)),
+                        NM_PRINT_FMT_QUOTE_STRING(address_string));
+        goto out;
+    }
+
+    /* Missing gateway not a hard failure */
+    gw_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv4_config);
+    if (gw_string && !nm_utils_parse_inaddr_bin(AF_INET, gw_string, NULL, &gw)) {
+        error =
+            g_error_new(NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INVALID_CONNECTION,
+                        "(%s) retrieving IP4 configuration failed: invalid gateway address \"%s\"",
+                        nm_modem_get_uid(NM_MODEM(self)),
+                        gw_string);
+        goto out;
+    }
+
+    data_port = mm_bearer_get_interface(self->_priv.bearer);
+    g_return_val_if_fail(data_port, FALSE);
+    config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
+                               nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port));
+
+    memset(&address, 0, sizeof(address));
+    address.address      = address_network;
+    address.peer_address = address_network;
+    address.plen         = mm_bearer_ip_config_get_prefix(self->_priv.ipv4_config);
+    address.addr_source  = NM_IP_CONFIG_SOURCE_WWAN;
+    if (address.plen <= 32)
+        nm_ip4_config_add_address(config, &address);
+
+    _LOGI("  address %s/%d", address_string, address.plen);
+
+    nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL);
+    r = &(NMPlatformIP4Route){
+        .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+        .gateway       = gw,
+        .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+        .metric        = ip4_route_metric,
+    };
+    nm_ip4_config_add_route(config, r, NULL);
+    _LOGI("  gateway %s", gw_string);
+
+    /* DNS servers */
+    dns = mm_bearer_ip_config_get_dns(self->_priv.ipv4_config);
+    for (i = 0; dns && dns[i]; i++) {
+        if (nm_utils_parse_inaddr_bin(AF_INET, dns[i], NULL, &address_network)
+            && address_network > 0) {
+            nm_ip4_config_add_nameserver(config, address_network);
+            _LOGI("  DNS %s", dns[i]);
+        }
+    }
+
+#if MM_CHECK_VERSION(1, 4, 0)
+    mtu_n = mm_bearer_ip_config_get_mtu(self->_priv.ipv4_config);
+    if (mtu_n) {
+        nm_ip4_config_set_mtu(config, mtu_n, NM_IP_CONFIG_SOURCE_WWAN);
+        _LOGI("  MTU %u", mtu_n);
+    }
+#endif
+
+out:
+    g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, config, error);
+    g_clear_error(&error);
+    return FALSE;
+}
+
+static NMActStageReturn
+static_stage3_ip4_config_start(NMModem *            modem,
+                               NMActRequest *       req,
+                               NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(modem);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    /* We schedule it in an idle just to follow the same logic as in the
+     * generic modem implementation. */
+    nm_clear_g_source(&priv->idle_id_ip4);
+    priv->idle_id_ip4 = g_idle_add((GSourceFunc) static_stage3_ip4_done, self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+/* IPv6 method static */
+
+static gboolean
+stage3_ip6_done(NMModemBroadband *self)
+{
+    GError *             error  = NULL;
+    NMIP6Config *        config = NULL;
+    const char *         data_port;
+    const char *         address_string;
+    NMPlatformIP6Address address;
+    NMModemIPMethod      ip_method;
+    const char **        dns;
+    guint                i;
+
+    g_return_val_if_fail(self->_priv.ipv6_config, FALSE);
+
+    self->_priv.idle_id_ip6 = 0;
+    memset(&address, 0, sizeof(address));
+
+    ip_method = get_bearer_ip_method(self->_priv.ipv6_config);
+
+    address_string = mm_bearer_ip_config_get_address(self->_priv.ipv6_config);
+    if (!address_string) {
+        /* DHCP/SLAAC is allowed to skip addresses; other methods require it */
+        if (ip_method != NM_MODEM_IP_METHOD_AUTO) {
+            error = g_error_new(NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INVALID_CONNECTION,
+                                "(%s) retrieving IPv6 configuration failed: no address given",
+                                nm_modem_get_uid(NM_MODEM(self)));
+        }
+        goto out;
+    }
+
+    /* Fail if invalid IP address retrieved */
+    if (!inet_pton(AF_INET6, address_string, (void *) &(address.address))) {
+        error = g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: invalid address given '%s'",
+                            nm_modem_get_uid(NM_MODEM(self)),
+                            address_string);
+        goto out;
+    }
+
+    _LOGI("IPv6 base configuration:");
+
+    data_port = mm_bearer_get_interface(self->_priv.bearer);
+    g_return_val_if_fail(data_port, FALSE);
+
+    config = nm_ip6_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
+                               nm_platform_link_get_ifindex(NM_PLATFORM_GET, data_port));
+
+    address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config);
+    if (address.plen <= 128)
+        nm_ip6_config_add_address(config, &address);
+
+    _LOGI("  address %s/%d", address_string, address.plen);
+
+    address_string = mm_bearer_ip_config_get_gateway(self->_priv.ipv6_config);
+    if (address_string) {
+        guint32 ip6_route_table, ip6_route_metric;
+
+        if (inet_pton(AF_INET6, address_string, &address.address) != 1) {
+            error =
+                g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: invalid gateway given '%s'",
+                            nm_modem_get_uid(NM_MODEM(self)),
+                            address_string);
+            goto out;
+        }
+
+        nm_modem_get_route_parameters(NM_MODEM(self),
+                                      NULL,
+                                      NULL,
+                                      &ip6_route_table,
+                                      &ip6_route_metric);
+        {
+            const NMPlatformIP6Route r = {
+                .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+                .gateway       = address.address,
+                .table_coerced = nm_platform_route_table_coerce(ip6_route_table),
+                .metric        = ip6_route_metric,
+            };
+
+            _LOGI("  gateway %s", address_string);
+            nm_ip6_config_add_route(config, &r, NULL);
+        }
+    } else if (ip_method == NM_MODEM_IP_METHOD_STATIC) {
+        /* Gateway required for the 'static' method */
+        error = g_error_new(NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INVALID_CONNECTION,
+                            "(%s) retrieving IPv6 configuration failed: missing gateway",
+                            nm_modem_get_uid(NM_MODEM(self)));
+        goto out;
+    }
+
+    /* DNS servers */
+    dns = mm_bearer_ip_config_get_dns(self->_priv.ipv6_config);
+    for (i = 0; dns && dns[i]; i++) {
+        struct in6_addr addr;
+
+        if (inet_pton(AF_INET6, dns[i], &addr)) {
+            nm_ip6_config_add_nameserver(config, &addr);
+            _LOGI("  DNS %s", dns[i]);
+        }
+    }
+
+out:
+    nm_modem_emit_ip6_config_result(NM_MODEM(self), config, error);
+    g_clear_object(&config);
+    g_clear_error(&error);
+    return FALSE;
+}
+
+static NMActStageReturn
+stage3_ip6_config_request(NMModem *modem, NMDeviceStateReason *out_failure_reason)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(modem);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    /* We schedule it in an idle just to follow the same logic as in the
+     * generic modem implementation. */
+    nm_clear_g_source(&priv->idle_id_ip6);
+    priv->idle_id_ip6 = g_idle_add((GSourceFunc) stage3_ip6_done, self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+/* Disconnect */
+
+typedef struct {
+    NMModemBroadband *         self;
+    _NMModemDisconnectCallback callback;
+    gpointer                   callback_user_data;
+    GCancellable *             cancellable;
+    gboolean                   warn;
+} DisconnectContext;
+
+static void
+disconnect_context_complete(DisconnectContext *ctx, GError *error)
+{
+    if (ctx->callback)
+        ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->self);
+    g_slice_free(DisconnectContext, ctx);
+}
+
+static void
+disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    DisconnectContext *ctx                = user_data;
+    gs_free_error GError *cancelled_error = NULL;
+
+    g_cancellable_set_error_if_cancelled(cancellable, &cancelled_error);
+    disconnect_context_complete(ctx, cancelled_error);
+}
+
+static void
+simple_disconnect_ready(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    MMModemSimple *    modem_iface = MM_MODEM_SIMPLE(source_object);
+    DisconnectContext *ctx         = user_data;
+    gs_free_error GError *error    = NULL;
+
+    if (!mm_modem_simple_disconnect_finish(modem_iface, res, &error)) {
+        if (ctx->warn && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) {
+            NMModemBroadband *self = ctx->self;
+
+            _LOGW("failed to disconnect modem: %s", error->message);
+        }
+    }
+
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect(NMModem *                  modem,
+           gboolean                   warn,
+           GCancellable *             cancellable,
+           _NMModemDisconnectCallback callback,
+           gpointer                   user_data)
+{
+    NMModemBroadband * self = NM_MODEM_BROADBAND(modem);
+    DisconnectContext *ctx;
+
+    connect_context_clear(self);
+    _nm_modem_set_apn(NM_MODEM(self), NULL);
+
+    ctx                     = g_slice_new0(DisconnectContext);
+    ctx->self               = g_object_ref(self);
+    ctx->cancellable        = nm_g_object_ref(cancellable);
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    /* Don't bother warning on FAILED since the modem is already gone */
+    ctx->warn = warn;
+
+    /* Already cancelled or no simple-iface? We are done. */
+    if (!ctx->self->_priv.simple_iface || g_cancellable_is_cancelled(cancellable)) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
+        return;
+    }
+
+    _LOGD("notifying ModemManager about the modem disconnection");
+    mm_modem_simple_disconnect(self->_priv.simple_iface,
+                               NULL, /* bearer path; if NULL given ALL get disconnected */
+                               cancellable,
+                               simple_disconnect_ready,
+                               ctx);
+}
+
+/*****************************************************************************/
+
+static void
+deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(modem);
+
+    /* TODO: cancel SimpleConnect() if any */
+
+    /* Cleanup IPv4 addresses and routes */
+    g_clear_object(&self->_priv.ipv4_config);
+    g_clear_object(&self->_priv.ipv6_config);
+    g_clear_object(&self->_priv.bearer);
+
+    self->_priv.pin_tries = 0;
+
+    NM_MODEM_CLASS(nm_modem_broadband_parent_class)
+        ->deactivate_cleanup(modem, device, stop_ppp_manager);
+}
+
+/*****************************************************************************/
+
+#define MAP_STATE(name)         \
+    case MM_MODEM_STATE_##name: \
+        return NM_MODEM_STATE_##name;
+
+static NMModemState
+mm_state_to_nm(MMModemState mm_state)
+{
+    switch (mm_state) {
+        MAP_STATE(UNKNOWN)
+        MAP_STATE(FAILED)
+        MAP_STATE(INITIALIZING)
+        MAP_STATE(LOCKED)
+        MAP_STATE(DISABLED)
+        MAP_STATE(DISABLING)
+        MAP_STATE(ENABLING)
+        MAP_STATE(ENABLED)
+        MAP_STATE(SEARCHING)
+        MAP_STATE(REGISTERED)
+        MAP_STATE(DISCONNECTING)
+        MAP_STATE(CONNECTING)
+        MAP_STATE(CONNECTED)
+    }
+    return NM_MODEM_STATE_UNKNOWN;
+}
+
+static void
+modem_state_changed(MMModem *                modem,
+                    MMModemState             old_state,
+                    MMModemState             new_state,
+                    MMModemStateChangeReason reason,
+                    NMModemBroadband *       self)
+{
+    /* After the SIM is unlocked MM1 will move the device to INITIALIZING which
+     * is an unavailable state.  That makes state handling confusing here, so
+     * suppress this state change and let the modem move from LOCKED to DISABLED.
+     */
+    if (new_state == MM_MODEM_STATE_INITIALIZING && old_state == MM_MODEM_STATE_LOCKED)
+        return;
+
+    nm_modem_set_state(NM_MODEM(self),
+                       mm_state_to_nm(new_state),
+                       mm_modem_state_change_reason_get_string(reason));
+
+    if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_READY)
+        connect_context_step(self);
+}
+
+/*****************************************************************************/
+
+static NMModemIPType
+mm_ip_family_to_nm(MMBearerIpFamily family)
+{
+    NMModemIPType nm_type = NM_MODEM_IP_TYPE_UNKNOWN;
+
+    if (family & MM_BEARER_IP_FAMILY_IPV4)
+        nm_type |= NM_MODEM_IP_TYPE_IPV4;
+    if (family & MM_BEARER_IP_FAMILY_IPV6)
+        nm_type |= NM_MODEM_IP_TYPE_IPV6;
+    if (family & MM_BEARER_IP_FAMILY_IPV4V6)
+        nm_type |= MM_BEARER_IP_FAMILY_IPV4V6;
+
+    return nm_type;
+}
+
+static void
+get_sim_ready(MMModem *modem, GAsyncResult *res, NMModemBroadband *self)
+{
+    GError *error = NULL;
+    MMSim * new_sim;
+
+    new_sim = mm_modem_get_sim_finish(modem, res, &error);
+    if (new_sim != self->_priv.sim_iface) {
+        g_clear_object(&self->_priv.sim_iface);
+        self->_priv.sim_iface = new_sim;
+    } else
+        g_clear_object(&new_sim);
+
+    if (self->_priv.sim_iface) {
+        g_object_set(G_OBJECT(self),
+                     NM_MODEM_SIM_ID,
+                     mm_sim_get_identifier(self->_priv.sim_iface),
+                     NM_MODEM_SIM_OPERATOR_ID,
+                     mm_sim_get_operator_identifier(self->_priv.sim_iface),
+                     NULL);
+
+        /* If we're waiting for the SIM during a connect, proceed with the connect */
+        if (self->_priv.ctx && self->_priv.ctx->step == CONNECT_STEP_WAIT_FOR_SIM)
+            connect_context_step(self);
+    } else {
+        _NMLOG(g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_NOT_FOUND) ? LOGL_INFO
+                                                                              : LOGL_WARN,
+               "failed to retrieve SIM object: %s",
+               NM_G_ERROR_MSG(error));
+    }
+    g_clear_error(&error);
+    g_object_unref(self);
+}
+
+static void
+sim_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem == self->_priv.modem_iface);
+
+    if (mm_modem_get_sim_path(self->_priv.modem_iface)) {
+        mm_modem_get_sim(self->_priv.modem_iface,
+                         NULL, /* cancellable */
+                         (GAsyncReadyCallback) get_sim_ready,
+                         g_object_ref(self));
+    } else
+        g_object_set(G_OBJECT(self), NM_MODEM_SIM_ID, NULL, NM_MODEM_SIM_OPERATOR_ID, NULL, NULL);
+}
+
+static void
+supported_ip_families_changed(MMModem *modem, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem == self->_priv.modem_iface);
+
+    g_object_set(G_OBJECT(self),
+                 NM_MODEM_IP_TYPES,
+                 mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem)),
+                 NULL);
+}
+
+static void
+operator_code_changed(MMModem3gpp *modem_3gpp, GParamSpec *pspec, gpointer user_data)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(user_data);
+
+    g_return_if_fail(modem_3gpp == self->_priv.modem_3gpp_iface);
+    _nm_modem_set_operator_code(NM_MODEM(self), mm_modem_3gpp_get_operator_code(modem_3gpp));
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        g_value_set_object(value, self->_priv.modem_object);
+        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)
+{
+    NMModemBroadband *self = NM_MODEM_BROADBAND(object);
+
+    switch (prop_id) {
+    case PROP_MODEM:
+        /* construct-only */
+        self->_priv.modem_object = g_value_dup_object(value);
+        self->_priv.modem_iface  = mm_object_get_modem(self->_priv.modem_object);
+        g_return_if_fail(self->_priv.modem_iface);
+        self->_priv.modem_3gpp_iface = mm_object_get_modem_3gpp(self->_priv.modem_object);
+
+        g_signal_connect(self->_priv.modem_iface,
+                         "state-changed",
+                         G_CALLBACK(modem_state_changed),
+                         self);
+        g_signal_connect(self->_priv.modem_iface, "notify::sim", G_CALLBACK(sim_changed), self);
+        sim_changed(self->_priv.modem_iface, NULL, self);
+        g_signal_connect(self->_priv.modem_iface,
+                         "notify::supported-ip-families",
+                         G_CALLBACK(supported_ip_families_changed),
+                         self);
+
+        if (self->_priv.modem_3gpp_iface) {
+            g_signal_connect(self->_priv.modem_3gpp_iface,
+                             "notify::operator-code",
+                             G_CALLBACK(operator_code_changed),
+                             self);
+        }
+
+        /* Note: don't grab the Simple iface here; the Modem interface is the
+         * only one assumed to be always valid and available */
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_broadband_init(NMModemBroadband *self)
+{}
+
+NMModem *
+nm_modem_broadband_new(GObject *object, GError **error)
+{
+    MMObject *         modem_object;
+    MMModem *          modem_iface;
+    MMModem3gpp *      modem_3gpp_iface;
+    const char *const *drivers;
+    const char *       operator_code = NULL;
+    gs_free char *     driver        = NULL;
+
+    g_return_val_if_fail(MM_IS_OBJECT(object), NULL);
+    modem_object = MM_OBJECT(object);
+
+    /* Ensure we have the 'Modem' interface and the primary port at least */
+    modem_iface = mm_object_peek_modem(modem_object);
+    g_return_val_if_fail(modem_iface, NULL);
+    g_return_val_if_fail(mm_modem_get_primary_port(modem_iface), NULL);
+
+    /* Build a single string with all drivers listed */
+    drivers = mm_modem_get_drivers(modem_iface);
+    if (drivers)
+        driver = g_strjoinv(", ", (char **) drivers);
+
+    modem_3gpp_iface = mm_object_peek_modem_3gpp(modem_object);
+    if (modem_3gpp_iface)
+        operator_code = mm_modem_3gpp_get_operator_code(modem_3gpp_iface);
+
+    return g_object_new(NM_TYPE_MODEM_BROADBAND,
+                        NM_MODEM_PATH,
+                        mm_object_get_path(modem_object),
+                        NM_MODEM_UID,
+                        mm_modem_get_primary_port(modem_iface),
+                        NM_MODEM_CONTROL_PORT,
+                        mm_modem_get_primary_port(modem_iface),
+                        NM_MODEM_IP_TYPES,
+                        mm_ip_family_to_nm(mm_modem_get_supported_ip_families(modem_iface)),
+                        NM_MODEM_STATE,
+                        (int) mm_state_to_nm(mm_modem_get_state(modem_iface)),
+                        NM_MODEM_DEVICE_ID,
+                        mm_modem_get_device_identifier(modem_iface),
+                        NM_MODEM_BROADBAND_MODEM,
+                        modem_object,
+                        NM_MODEM_DRIVER,
+                        driver,
+                        NM_MODEM_OPERATOR_CODE,
+                        operator_code,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemBroadband *       self = NM_MODEM_BROADBAND(object);
+    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+
+    nm_clear_g_source(&priv->idle_id_ip4);
+    nm_clear_g_source(&priv->idle_id_ip6);
+
+    connect_context_clear(self);
+    g_clear_object(&self->_priv.ipv4_config);
+    g_clear_object(&self->_priv.ipv6_config);
+    g_clear_object(&self->_priv.bearer);
+
+    if (self->_priv.modem_iface) {
+        g_signal_handlers_disconnect_by_data(self->_priv.modem_iface, self);
+        g_clear_object(&self->_priv.modem_iface);
+    }
+
+    if (self->_priv.modem_3gpp_iface) {
+        g_signal_handlers_disconnect_by_data(self->_priv.modem_3gpp_iface, self);
+        g_clear_object(&self->_priv.modem_3gpp_iface);
+    }
+
+    g_clear_object(&self->_priv.simple_iface);
+    g_clear_object(&self->_priv.sim_iface);
+    g_clear_object(&self->_priv.modem_object);
+
+    G_OBJECT_CLASS(nm_modem_broadband_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_broadband_class_init(NMModemBroadbandClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+    NMModemClass *modem_class  = NM_MODEM_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+    object_class->set_property = set_property;
+
+    modem_class->get_capabilities                       = get_capabilities;
+    modem_class->static_stage3_ip4_config_start         = static_stage3_ip4_config_start;
+    modem_class->stage3_ip6_config_request              = stage3_ip6_config_request;
+    modem_class->disconnect                             = disconnect;
+    modem_class->deactivate_cleanup                     = deactivate_cleanup;
+    modem_class->set_mm_enabled                         = set_mm_enabled;
+    modem_class->get_user_pass                          = get_user_pass;
+    modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem;
+    modem_class->complete_connection                    = complete_connection;
+    modem_class->modem_act_stage1_prepare               = modem_act_stage1_prepare;
+    modem_class->owns_port                              = owns_port;
+
+    obj_properties[PROP_MODEM] =
+        g_param_spec_object(NM_MODEM_BROADBAND_MODEM,
+                            "",
+                            "",
+                            MM_GDBUS_TYPE_OBJECT,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/src/core/devices/wwan/nm-modem-broadband.h b/src/core/devices/wwan/nm-modem-broadband.h
new file mode 100644
index 00000000..627fe25a
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-broadband.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2012 - Aleksander Morgado <aleksander@gnu.org>
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_BROADBAND_H__
+#define __NETWORKMANAGER_MODEM_BROADBAND_H__
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_BROADBAND (nm_modem_broadband_get_type())
+#define NM_MODEM_BROADBAND(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadband))
+#define NM_MODEM_BROADBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass))
+#define NM_IS_MODEM_BROADBAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_BROADBAND))
+#define NM_IS_MODEM_BROADBAND_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_BROADBAND))
+#define NM_MODEM_BROADBAND_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_BROADBAND, NMModemBroadbandClass))
+
+typedef struct _NMModemBroadband      NMModemBroadband;
+typedef struct _NMModemBroadbandClass NMModemBroadbandClass;
+
+GType nm_modem_broadband_get_type(void);
+
+NMModem *nm_modem_broadband_new(GObject *object, GError **error);
+
+#endif /* __NETWORKMANAGER_MODEM_BROADBAND_H__ */
diff --git a/src/core/devices/wwan/nm-modem-manager.c b/src/core/devices/wwan/nm-modem-manager.c
new file mode 100644
index 00000000..598c6898
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-manager.c
@@ -0,0 +1,876 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ * Copyright (C) 2009 - 2013 Canonical Ltd.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-manager.h"
+
+#include <libmm-glib.h>
+
+#if HAVE_LIBSYSTEMD
+    #include <systemd/sd-daemon.h>
+#else
+    #define sd_booted() FALSE
+#endif
+
+#include "nm-std-aux/nm-dbus-compat.h"
+#include "nm-modem.h"
+#include "nm-modem-broadband.h"
+
+#if WITH_OFONO
+    #include "nm-modem-ofono.h"
+#endif
+
+#define MODEM_POKE_INTERVAL 120
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMModemManager, PROP_NAME_OWNER, );
+
+enum {
+    MODEM_ADDED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct {
+    GDBusConnection *dbus_connection;
+
+    /* used during g_bus_get() and later during mm_manager_new(). */
+    GCancellable *main_cancellable;
+
+    struct {
+        MMManager *   manager;
+        GCancellable *poke_cancellable;
+        gulong        handle_name_owner_changed_id;
+        gulong        handle_object_added_id;
+        gulong        handle_object_removed_id;
+        guint         relaunch_id;
+
+        /* this only has one use: that the <info> logging line about
+         * ModemManager available distinguishes between first-time
+         * and later name-owner-changed. */
+        enum {
+            LOG_AVAILABLE_NOT_INITIALIZED = 0,
+            LOG_AVAILABLE_YES,
+            LOG_AVAILABLE_NO,
+        } log_available : 3;
+
+        GDBusProxy *  proxy;
+        GCancellable *proxy_cancellable;
+        guint         proxy_ref_count;
+        char *        proxy_name_owner;
+    } modm;
+
+#if WITH_OFONO
+    struct {
+        GDBusProxy *  proxy;
+        GCancellable *cancellable;
+    } ofono;
+#endif
+
+    GHashTable *modems;
+} NMModemManagerPrivate;
+
+struct _NMModemManager {
+    GObject               parent;
+    NMModemManagerPrivate _priv;
+};
+
+struct _NMModemManagerClass {
+    GObjectClass parent;
+};
+
+G_DEFINE_TYPE(NMModemManager, nm_modem_manager, G_TYPE_OBJECT)
+
+#define NM_MODEM_MANAGER_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMModemManager, NM_IS_MODEM_MANAGER)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "modem-manager", __VA_ARGS__)
+
+/*****************************************************************************/
+
+NM_DEFINE_SINGLETON_GETTER(NMModemManager, nm_modem_manager_get, NM_TYPE_MODEM_MANAGER);
+
+/*****************************************************************************/
+
+static void modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds);
+static void modm_ensure_manager(NMModemManager *self);
+
+/*****************************************************************************/
+
+static void
+handle_new_modem(NMModemManager *self, NMModem *modem)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    const char *           path;
+
+    path = nm_modem_get_path(modem);
+    if (g_hash_table_lookup(priv->modems, path)) {
+        g_warn_if_reached();
+        return;
+    }
+
+    /* Track the new modem */
+    g_hash_table_insert(priv->modems, g_strdup(path), modem);
+    g_signal_emit(self, signals[MODEM_ADDED], 0, modem);
+}
+
+static gboolean
+remove_one_modem(gpointer key, gpointer value, gpointer user_data)
+{
+    nm_modem_emit_removed(NM_MODEM(value));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+NMModem **
+nm_modem_manager_get_modems(NMModemManager *self, guint *out_len)
+{
+    g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL);
+
+    return (NMModem **) nm_utils_hash_values_to_array(NM_MODEM_MANAGER_GET_PRIVATE(self)->modems,
+                                                      NULL,
+                                                      NULL,
+                                                      out_len);
+}
+
+/*****************************************************************************/
+
+static void
+modm_clear_manager(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (!priv->modm.manager)
+        return;
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_name_owner_changed_id);
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_added_id);
+    nm_clear_g_signal_handler(priv->modm.manager, &priv->modm.handle_object_removed_id);
+    g_clear_object(&priv->modm.manager);
+}
+
+static void
+modm_handle_object_added(MMManager *modem_manager, MMObject *modem_object, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    const char *           path;
+    MMModem *              modem_iface;
+    NMModem *              modem;
+    GError *               error = NULL;
+
+    /* Ensure we don't have the same modem already */
+    path = mm_object_get_path(modem_object);
+    if (g_hash_table_lookup(priv->modems, path)) {
+        _LOGW("modem with path %s already exists, ignoring", path);
+        return;
+    }
+
+    /* Ensure we have the 'Modem' interface at least */
+    modem_iface = mm_object_peek_modem(modem_object);
+    if (!modem_iface) {
+        _LOGW("modem with path %s doesn't have the Modem interface, ignoring", path);
+        return;
+    }
+
+    /* Ensure we have a primary port reported */
+    if (!mm_modem_get_primary_port(modem_iface)) {
+        _LOGW("modem with path %s has unknown primary port, ignoring", path);
+        return;
+    }
+
+    /* Create a new modem object */
+    modem = nm_modem_broadband_new(G_OBJECT(modem_object), &error);
+    if (modem)
+        handle_new_modem(self, modem);
+    else
+        _LOGW("failed to create modem: %s", error->message);
+    g_clear_error(&error);
+}
+
+static void
+modm_handle_object_removed(MMManager *manager, MMObject *modem_object, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    NMModem *              modem;
+    const char *           path;
+
+    path  = mm_object_get_path(modem_object);
+    modem = (NMModem *) g_hash_table_lookup(priv->modems, path);
+    if (!modem)
+        return;
+
+    nm_modem_emit_removed(modem);
+    g_hash_table_remove(priv->modems, path);
+}
+
+static void
+modm_manager_available(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    GList *                modems, *l;
+
+    if (priv->modm.log_available != LOG_AVAILABLE_YES) {
+        _LOGI("ModemManager %savailable", priv->modm.log_available ? "now " : "");
+        priv->modm.log_available = LOG_AVAILABLE_YES;
+    }
+
+    /* Update initial modems list */
+    modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(priv->modm.manager));
+    for (l = modems; l; l = g_list_next(l))
+        modm_handle_object_added(priv->modm.manager, MM_OBJECT(l->data), self);
+    g_list_free_full(modems, (GDestroyNotify) g_object_unref);
+}
+
+static void
+modm_handle_name_owner_changed(MMManager *modem_manager, GParamSpec *pspec, NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 name_owner;
+
+    /* Quit poking, if any */
+    nm_clear_g_source(&priv->modm.relaunch_id);
+
+    name_owner =
+        g_dbus_object_manager_client_get_name_owner(G_DBUS_OBJECT_MANAGER_CLIENT(modem_manager));
+    if (!name_owner) {
+        if (priv->modm.log_available != LOG_AVAILABLE_NO) {
+            _LOGI("ModemManager %savailable", priv->modm.log_available ? "no longer " : "not ");
+            priv->modm.log_available = LOG_AVAILABLE_NO;
+        }
+
+        /* If not managed by systemd, schedule relaunch */
+        if (!sd_booted())
+            modm_schedule_manager_relaunch(self, 0);
+
+        return;
+    }
+
+    /* Available! */
+    g_free(name_owner);
+
+    /* Hack alert: GDBusObjectManagerClient won't signal neither 'object-added'
+     * nor 'object-removed' if it was created while there was no ModemManager in
+     * the bus. This hack avoids this issue until we get a GIO with the fix
+     * included... */
+    modm_clear_manager(self);
+    modm_ensure_manager(self);
+
+    /* Whenever GDBusObjectManagerClient is fixed, we can just do the following:
+     * modm_manager_available (self);
+     */
+}
+
+static void
+modm_manager_poke_cb(GObject *connection, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error       = NULL;
+    gs_unref_variant GVariant *result = NULL;
+
+    result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(connection), res, &error);
+
+    if (!result && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modm.poke_cancellable);
+
+    if (error) {
+        _LOGW("error poking ModemManager: %s", error->message);
+
+        /* Don't reschedule poke is MM service doesn't exist. */
+        if (!g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)
+            && !g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_SPAWN_SERVICE_NOT_FOUND)) {
+            /* Setup timeout to relaunch */
+            modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL);
+        }
+    }
+}
+
+static void
+modm_manager_poke(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->modm.poke_cancellable);
+    priv->modm.poke_cancellable = g_cancellable_new();
+
+    /* If there is no current owner right away, ensure we poke to get one */
+    g_dbus_connection_call(priv->dbus_connection,
+                           NM_MODEM_MANAGER_MM_DBUS_SERVICE,
+                           NM_MODEM_MANAGER_MM_DBUS_PATH,
+                           DBUS_INTERFACE_PEER,
+                           "Ping",
+                           NULL,
+                           NULL,
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           priv->modm.poke_cancellable,
+                           modm_manager_poke_cb,
+                           self);
+}
+
+static void
+modm_manager_check_name_owner(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv       = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    gs_free char *         name_owner = NULL;
+
+    name_owner = g_dbus_object_manager_client_get_name_owner(
+        G_DBUS_OBJECT_MANAGER_CLIENT(priv->modm.manager));
+    if (name_owner) {
+        modm_manager_available(self);
+        return;
+    }
+
+    /* If the lifecycle is not managed by systemd, poke */
+    if (!sd_booted())
+        modm_manager_poke(self);
+}
+
+static void
+modm_manager_new_cb(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    MMManager *           modem_manager;
+
+    modem_manager = mm_manager_new_finish(res, &error);
+    if (!modem_manager && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(!priv->modm.manager);
+
+    g_clear_object(&priv->main_cancellable);
+
+    if (!modem_manager) {
+        /* We're not really supposed to get any error here. If we do get one,
+         * though, just re-schedule the MMManager creation after some time.
+         * During this period, name-owner changes won't be followed. */
+        _LOGW("error creating ModemManager client: %s", error->message);
+        /* Setup timeout to relaunch */
+        modm_schedule_manager_relaunch(self, MODEM_POKE_INTERVAL);
+        return;
+    }
+
+    priv->modm.manager = modem_manager;
+
+    /* Setup signals in the GDBusObjectManagerClient */
+    priv->modm.handle_name_owner_changed_id =
+        g_signal_connect(priv->modm.manager,
+                         "notify::name-owner",
+                         G_CALLBACK(modm_handle_name_owner_changed),
+                         self);
+    priv->modm.handle_object_added_id   = g_signal_connect(priv->modm.manager,
+                                                         "object-added",
+                                                         G_CALLBACK(modm_handle_object_added),
+                                                         self);
+    priv->modm.handle_object_removed_id = g_signal_connect(priv->modm.manager,
+                                                           "object-removed",
+                                                           G_CALLBACK(modm_handle_object_removed),
+                                                           self);
+
+    modm_manager_check_name_owner(self);
+}
+
+static void
+modm_ensure_manager(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_assert(priv->dbus_connection);
+
+    /* Create the GDBusObjectManagerClient. We do not request to autostart, as
+     * we don't really want the MMManager creation to fail. We can always poke
+     * later on if we want to request the autostart */
+    if (!priv->modm.manager) {
+        if (!priv->main_cancellable)
+            priv->main_cancellable = g_cancellable_new();
+        mm_manager_new(priv->dbus_connection,
+                       G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
+                       priv->main_cancellable,
+                       modm_manager_new_cb,
+                       self);
+        return;
+    }
+
+    /* If already available, recheck name owner! */
+    modm_manager_check_name_owner(self);
+}
+
+static gboolean
+modm_schedule_manager_relaunch_cb(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    priv->modm.relaunch_id = 0;
+    modm_ensure_manager(self);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+modm_schedule_manager_relaunch(NMModemManager *self, guint n_seconds)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    /* No need to pass an extra reference to self; timeout/idle will be
+     * cancelled if the object gets disposed. */
+    if (n_seconds)
+        priv->modm.relaunch_id =
+            g_timeout_add_seconds(n_seconds, (GSourceFunc) modm_schedule_manager_relaunch_cb, self);
+    else
+        priv->modm.relaunch_id = g_idle_add((GSourceFunc) modm_schedule_manager_relaunch_cb, self);
+}
+
+/*****************************************************************************/
+
+static void
+modm_proxy_name_owner_reset(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 name = NULL;
+
+    if (priv->modm.proxy)
+        name = g_dbus_proxy_get_name_owner(priv->modm.proxy);
+
+    if (nm_streq0(priv->modm.proxy_name_owner, name)) {
+        g_free(name);
+        return;
+    }
+    g_free(priv->modm.proxy_name_owner);
+    priv->modm.proxy_name_owner = name;
+
+    _notify(self, PROP_NAME_OWNER);
+}
+
+static void
+modm_proxy_name_owner_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    modm_proxy_name_owner_reset(user_data);
+}
+
+static void
+modm_proxy_new_cb(GObject *source_object, GAsyncResult *result, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    GDBusProxy *           proxy;
+    gs_free_error GError *error = NULL;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modm.proxy_cancellable);
+
+    if (!proxy) {
+        _LOGW("could not obtain D-Bus proxy for ModemManager: %s", error->message);
+        return;
+    }
+
+    priv->modm.proxy = proxy;
+    g_signal_connect(priv->modm.proxy,
+                     "notify::g-name-owner",
+                     G_CALLBACK(modm_proxy_name_owner_changed_cb),
+                     self);
+
+    modm_proxy_name_owner_reset(self);
+}
+
+void
+nm_modem_manager_name_owner_ref(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM_MANAGER(self));
+
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (priv->modm.proxy_ref_count++ > 0) {
+        /* only try once to create the proxy. If proxy creation
+         * for the first "ref" failed, it's unclear what to do.
+         * The proxy is hosed. */
+        return;
+    }
+
+    nm_assert(!priv->modm.proxy && !priv->modm.proxy_cancellable);
+
+    priv->modm.proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                 | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS
+                                 | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             NM_MODEM_MANAGER_MM_DBUS_SERVICE,
+                             NM_MODEM_MANAGER_MM_DBUS_PATH,
+                             NM_MODEM_MANAGER_MM_DBUS_INTERFACE,
+                             priv->modm.proxy_cancellable,
+                             modm_proxy_new_cb,
+                             self);
+}
+
+void
+nm_modem_manager_name_owner_unref(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM_MANAGER(self));
+
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->modm.proxy_ref_count > 0);
+
+    if (--priv->modm.proxy_ref_count > 0)
+        return;
+
+    nm_clear_g_cancellable(&priv->modm.proxy_cancellable);
+    g_clear_object(&priv->modm.proxy);
+
+    modm_proxy_name_owner_reset(self);
+}
+
+const char *
+nm_modem_manager_name_owner_get(NMModemManager *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM_MANAGER(self), NULL);
+    nm_assert(NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_ref_count > 0);
+
+    return NM_MODEM_MANAGER_GET_PRIVATE(self)->modm.proxy_name_owner;
+}
+
+/*****************************************************************************/
+
+#if WITH_OFONO
+
+static void
+ofono_create_modem(NMModemManager *self, const char *path)
+{
+    NMModemManagerPrivate *priv  = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    NMModem *              modem = NULL;
+
+    /* Ensure duplicate modems aren't created.  Because we're not using the
+     * ObjectManager interface there's a race during oFono startup where we
+     * receive ModemAdded signals before GetModems() returns, so some of the
+     * modems returned from GetModems() may already have been created.
+     */
+    if (!g_hash_table_lookup(priv->modems, path)) {
+        modem = nm_modem_ofono_new(path);
+        if (modem)
+            handle_new_modem(self, modem);
+        else
+            _LOGW("Failed to create oFono modem for %s", path);
+    }
+}
+
+static void
+ofono_signal_cb(GDBusProxy *proxy,
+                char *      sender_name,
+                char *      signal_name,
+                GVariant *  parameters,
+                gpointer    user_data)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(user_data);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    char *                 object_path;
+    NMModem *              modem;
+
+    if (g_strcmp0(signal_name, "ModemAdded") == 0) {
+        g_variant_get(parameters, "(oa{sv})", &object_path, NULL);
+        _LOGI("oFono modem appeared: %s", object_path);
+
+        ofono_create_modem(NM_MODEM_MANAGER(user_data), object_path);
+        g_free(object_path);
+    } else if (g_strcmp0(signal_name, "ModemRemoved") == 0) {
+        g_variant_get(parameters, "(o)", &object_path);
+        _LOGI("oFono modem removed: %s", object_path);
+
+        modem = (NMModem *) g_hash_table_lookup(priv->modems, object_path);
+        if (modem) {
+            nm_modem_emit_removed(modem);
+            g_hash_table_remove(priv->modems, object_path);
+        } else {
+            _LOGW("could not remove modem %s, not found in table", object_path);
+        }
+        g_free(object_path);
+    }
+}
+
+static void
+ofono_enumerate_devices_done(GObject *proxy, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GVariant *            results;
+    GVariantIter *        iter;
+    const char *          path;
+
+    results = g_dbus_proxy_call_finish(G_DBUS_PROXY(proxy), res, &error);
+    if (!results && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->ofono.cancellable);
+
+    if (!results) {
+        _LOGW("failed to enumerate oFono devices: %s", error->message);
+        return;
+    }
+
+    g_variant_get(results, "(a(oa{sv}))", &iter);
+    while (g_variant_iter_loop(iter, "(&oa{sv})", &path, NULL))
+        ofono_create_modem(self, path);
+    g_variant_iter_free(iter);
+    g_variant_unref(results);
+}
+
+static void
+ofono_check_name_owner(NMModemManager *self, gboolean first_invocation)
+{
+    NMModemManagerPrivate *priv       = NM_MODEM_MANAGER_GET_PRIVATE(self);
+    gs_free char *         name_owner = NULL;
+
+    name_owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(priv->ofono.proxy));
+    if (name_owner) {
+        _LOGI("oFono is %savailable", first_invocation ? "" : "now ");
+
+        nm_clear_g_cancellable(&priv->ofono.cancellable);
+        priv->ofono.cancellable = g_cancellable_new();
+
+        g_dbus_proxy_call(priv->ofono.proxy,
+                          "GetModems",
+                          NULL,
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1,
+                          priv->ofono.cancellable,
+                          ofono_enumerate_devices_done,
+                          self);
+    } else {
+        GHashTableIter iter;
+        NMModem *      modem;
+
+        _LOGI("oFono is %savailable", first_invocation ? "not " : "no longer ");
+
+        /* Remove any oFono modems that might be left around */
+        g_hash_table_iter_init(&iter, priv->modems);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer) &modem)) {
+            if (NM_IS_MODEM_OFONO(modem)) {
+                nm_modem_emit_removed(modem);
+                g_hash_table_iter_remove(&iter);
+            }
+        }
+    }
+}
+
+static void
+ofono_name_owner_changed(GDBusProxy *ofono_proxy, GParamSpec *pspec, NMModemManager *self)
+{
+    ofono_check_name_owner(self, FALSE);
+}
+
+static void
+ofono_proxy_new_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_finish(res, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    g_clear_object(&priv->ofono.cancellable);
+
+    if (!proxy) {
+        _LOGW("error getting oFono bus proxy: %s", error->message);
+        return;
+    }
+
+    priv->ofono.proxy = proxy;
+
+    g_signal_connect(priv->ofono.proxy,
+                     "notify::g-name-owner",
+                     G_CALLBACK(ofono_name_owner_changed),
+                     self);
+
+    g_signal_connect(priv->ofono.proxy, "g-signal", G_CALLBACK(ofono_signal_cb), self);
+
+    ofono_check_name_owner(self, TRUE);
+}
+
+static void
+ofono_init_proxy(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(priv->dbus_connection);
+    nm_assert(!priv->ofono.cancellable);
+
+    priv->ofono.cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new(priv->dbus_connection,
+                     G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                     NULL,
+                     OFONO_DBUS_SERVICE,
+                     OFONO_DBUS_PATH,
+                     OFONO_DBUS_INTERFACE,
+                     priv->ofono.cancellable,
+                     ofono_proxy_new_cb,
+                     self);
+}
+#endif
+
+/*****************************************************************************/
+
+static void
+bus_get_ready(GObject *source, GAsyncResult *res, gpointer user_data)
+{
+    NMModemManager *       self;
+    NMModemManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusConnection *     connection;
+
+    connection = g_bus_get_finish(res, &error);
+    if (!connection && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_MANAGER(user_data);
+    priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    if (!connection) {
+        _LOGW("error getting bus connection: %s", error->message);
+        return;
+    }
+
+    priv->dbus_connection = connection;
+
+    modm_ensure_manager(self);
+#if WITH_OFONO
+    ofono_init_proxy(self);
+#endif
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(object);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_NAME_OWNER:
+        g_value_set_string(value, priv->modm.proxy_name_owner);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_manager_init(NMModemManager *self)
+{
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    priv->modems = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_object_unref);
+
+    priv->main_cancellable = g_cancellable_new();
+
+    g_bus_get(G_BUS_TYPE_SYSTEM, priv->main_cancellable, bus_get_ready, self);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemManager *       self = NM_MODEM_MANAGER(object);
+    NMModemManagerPrivate *priv = NM_MODEM_MANAGER_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->main_cancellable);
+    nm_clear_g_cancellable(&priv->modm.poke_cancellable);
+
+    nm_clear_g_source(&priv->modm.relaunch_id);
+
+    nm_clear_g_cancellable(&priv->modm.proxy_cancellable);
+    g_clear_object(&priv->modm.proxy);
+    nm_clear_g_free(&priv->modm.proxy_name_owner);
+
+    modm_clear_manager(self);
+
+#if WITH_OFONO
+    if (priv->ofono.proxy) {
+        g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_name_owner_changed, self);
+        g_signal_handlers_disconnect_by_func(priv->ofono.proxy, ofono_signal_cb, self);
+        g_clear_object(&priv->ofono.proxy);
+    }
+    nm_clear_g_cancellable(&priv->ofono.cancellable);
+#endif
+
+    g_clear_object(&priv->dbus_connection);
+
+    if (priv->modems) {
+        g_hash_table_foreach_remove(priv->modems, remove_one_modem, object);
+        g_hash_table_destroy(priv->modems);
+        priv->modems = NULL;
+    }
+
+    G_OBJECT_CLASS(nm_modem_manager_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_manager_class_init(NMModemManagerClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    object_class->dispose      = dispose;
+    object_class->get_property = get_property;
+
+    obj_properties[PROP_NAME_OWNER] =
+        g_param_spec_string(NM_MODEM_MANAGER_NAME_OWNER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[MODEM_ADDED] = g_signal_new(NM_MODEM_MANAGER_MODEM_ADDED,
+                                        G_OBJECT_CLASS_TYPE(object_class),
+                                        G_SIGNAL_RUN_FIRST,
+                                        0,
+                                        NULL,
+                                        NULL,
+                                        NULL,
+                                        G_TYPE_NONE,
+                                        1,
+                                        NM_TYPE_MODEM);
+}
diff --git a/src/core/devices/wwan/nm-modem-manager.h b/src/core/devices/wwan/nm-modem-manager.h
new file mode 100644
index 00000000..e89a032b
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-manager.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ * Copyright (C) 2009 Canonical Ltd.
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_MANAGER_H__
+#define __NETWORKMANAGER_MODEM_MANAGER_H__
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_MANAGER (nm_modem_manager_get_type())
+#define NM_MODEM_MANAGER(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_MANAGER, NMModemManager))
+#define NM_MODEM_MANAGER_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_MANAGER, NMModemManagerClass))
+#define NM_IS_MODEM_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_MANAGER))
+#define NM_IS_MODEM_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_MANAGER))
+#define NM_MODEM_MANAGER_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_MANAGER, NMModemManagerClass))
+
+#define NM_MODEM_MANAGER_MODEM_ADDED "modem-added"
+
+#define NM_MODEM_MANAGER_NAME_OWNER "name-owner"
+
+#define NM_MODEM_MANAGER_MM_DBUS_SERVICE   "org.freedesktop.ModemManager1"
+#define NM_MODEM_MANAGER_MM_DBUS_PATH      "/org/freedesktop/ModemManager1"
+#define NM_MODEM_MANAGER_MM_DBUS_INTERFACE "org.freedesktop.ModemManager1"
+
+typedef struct _NMModemManager      NMModemManager;
+typedef struct _NMModemManagerClass NMModemManagerClass;
+
+GType nm_modem_manager_get_type(void);
+
+NMModemManager *nm_modem_manager_get(void);
+
+void nm_modem_manager_name_owner_ref(NMModemManager *self);
+void nm_modem_manager_name_owner_unref(NMModemManager *self);
+
+const char *nm_modem_manager_name_owner_get(NMModemManager *self);
+
+NMModem **nm_modem_manager_get_modems(NMModemManager *self, guint *out_len);
+
+#endif /* __NETWORKMANAGER_MODEM_MANAGER_H__ */
diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c
new file mode 100644
index 00000000..21734cee
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-ofono.c
@@ -0,0 +1,1239 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - 2016 Canonical Ltd.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem-ofono.h"
+
+#include "nm-core-internal.h"
+#include "devices/nm-device-private.h"
+#include "nm-modem.h"
+#include "platform/nm-platform.h"
+#include "nm-ip4-config.h"
+
+#define VARIANT_IS_OF_TYPE_BOOLEAN(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_BOOLEAN)))
+#define VARIANT_IS_OF_TYPE_STRING(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING)))
+#define VARIANT_IS_OF_TYPE_OBJECT_PATH(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_OBJECT_PATH)))
+#define VARIANT_IS_OF_TYPE_STRING_ARRAY(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_STRING_ARRAY)))
+#define VARIANT_IS_OF_TYPE_DICTIONARY(v) \
+    ((v) != NULL && (g_variant_is_of_type((v), G_VARIANT_TYPE_DICTIONARY)))
+
+/*****************************************************************************/
+
+typedef struct {
+    GHashTable *connect_properties;
+
+    GDBusProxy *modem_proxy;
+    GDBusProxy *connman_proxy;
+    GDBusProxy *context_proxy;
+    GDBusProxy *sim_proxy;
+
+    GCancellable *modem_proxy_cancellable;
+    GCancellable *connman_proxy_cancellable;
+    GCancellable *context_proxy_cancellable;
+    GCancellable *sim_proxy_cancellable;
+
+    GError *property_error;
+
+    char *context_path;
+    char *imsi;
+
+    gboolean modem_online;
+    gboolean gprs_attached;
+
+    NMIP4Config *ip4_config;
+} NMModemOfonoPrivate;
+
+struct _NMModemOfono {
+    NMModem             parent;
+    NMModemOfonoPrivate _priv;
+};
+
+struct _NMModemOfonoClass {
+    NMModemClass parent;
+};
+
+G_DEFINE_TYPE(NMModemOfono, nm_modem_ofono, NM_TYPE_MODEM)
+
+#define NM_MODEM_OFONO_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMModemOfono, NM_IS_MODEM_OFONO)
+
+/*****************************************************************************/
+
+#define _NMLOG_DOMAIN      LOGD_MB
+#define _NMLOG_PREFIX_NAME "modem-ofono"
+#define _NMLOG(level, ...)                                                  \
+    G_STMT_START                                                            \
+    {                                                                       \
+        const NMLogLevel _level = (level);                                  \
+                                                                            \
+        if (nm_logging_enabled(_level, (_NMLOG_DOMAIN))) {                  \
+            NMModemOfono *const __self = (self);                            \
+            char                __prefix_name[128];                         \
+            const char *        __uid;                                      \
+                                                                            \
+            _nm_log(_level,                                                 \
+                    (_NMLOG_DOMAIN),                                        \
+                    0,                                                      \
+                    NULL,                                                   \
+                    NULL,                                                   \
+                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
+                    _NMLOG_PREFIX_NAME,                                     \
+                    (__self ? ({                                            \
+                        ((__uid = nm_modem_get_uid((NMModem *) __self))     \
+                             ? nm_sprintf_buf(__prefix_name, "[%s]", __uid) \
+                             : "(null)");                                   \
+                    })                                                      \
+                            : "") _NM_UTILS_MACRO_REST(__VA_ARGS__));       \
+        }                                                                   \
+    }                                                                       \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void
+get_capabilities(NMModem *                  _self,
+                 NMDeviceModemCapabilities *modem_caps,
+                 NMDeviceModemCapabilities *current_caps)
+{
+    /* FIXME: auto-detect capabilities to allow LTE */
+    *modem_caps   = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
+    *current_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
+}
+
+static void
+update_modem_state(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv      = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMModemState         state     = nm_modem_get_state(NM_MODEM(self));
+    NMModemState         new_state = NM_MODEM_STATE_DISABLED;
+    const char *         reason    = NULL;
+
+    _LOGI("'Attached': %s 'Online': %s 'IMSI': %s",
+          priv->gprs_attached ? "true" : "false",
+          priv->modem_online ? "true" : "false",
+          priv->imsi);
+
+    if (priv->modem_online == FALSE) {
+        reason = "modem 'Online=false'";
+    } else if (priv->imsi == NULL && state != NM_MODEM_STATE_ENABLING) {
+        reason = "modem not ready";
+    } else if (priv->gprs_attached == FALSE) {
+        new_state = NM_MODEM_STATE_SEARCHING;
+        reason    = "modem searching";
+    } else {
+        new_state = NM_MODEM_STATE_REGISTERED;
+        reason    = "modem ready";
+    }
+
+    if (state != new_state)
+        nm_modem_set_state(NM_MODEM(self), new_state, reason);
+}
+
+/* Disconnect */
+typedef struct {
+    NMModemOfono *             self;
+    _NMModemDisconnectCallback callback;
+    gpointer                   callback_user_data;
+    GCancellable *             cancellable;
+    gboolean                   warn;
+} DisconnectContext;
+
+static void
+disconnect_context_complete(DisconnectContext *ctx, GError *error)
+{
+    if (ctx->callback)
+        ctx->callback(NM_MODEM(ctx->self), error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->self);
+    g_slice_free(DisconnectContext, ctx);
+}
+
+static void
+disconnect_context_complete_on_idle(gpointer user_data, GCancellable *cancellable)
+{
+    DisconnectContext *ctx      = user_data;
+    gs_free_error GError *error = NULL;
+
+    if (!g_cancellable_set_error_if_cancelled(cancellable, &error)) {
+        g_set_error_literal(&error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            ("modem is currently not connected"));
+    }
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    DisconnectContext *ctx       = user_data;
+    NMModemOfono *     self      = ctx->self;
+    gs_free_error GError *error  = NULL;
+    gs_unref_variant GVariant *v = NULL;
+
+    v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error);
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        disconnect_context_complete(ctx, error);
+        return;
+    }
+
+    if (error && ctx->warn)
+        _LOGW("failed to disconnect modem: %s", error->message);
+
+    _LOGD("modem disconnected");
+
+    update_modem_state(self);
+    disconnect_context_complete(ctx, error);
+}
+
+static void
+disconnect(NMModem *                  modem,
+           gboolean                   warn,
+           GCancellable *             cancellable,
+           _NMModemDisconnectCallback callback,
+           gpointer                   user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    DisconnectContext *  ctx;
+    NMModemState         state = nm_modem_get_state(NM_MODEM(self));
+
+    _LOGD("warn: %s modem_state: %s", warn ? "TRUE" : "FALSE", nm_modem_state_to_string(state));
+
+    ctx                     = g_slice_new0(DisconnectContext);
+    ctx->self               = g_object_ref(self);
+    ctx->cancellable        = nm_g_object_ref(cancellable);
+    ctx->warn               = warn;
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)) {
+        nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
+        return;
+    }
+
+    nm_modem_set_state(NM_MODEM(self),
+                       NM_MODEM_STATE_DISCONNECTING,
+                       nm_modem_state_to_string(NM_MODEM_STATE_DISCONNECTING));
+
+    g_dbus_proxy_call(priv->context_proxy,
+                      "SetProperty",
+                      g_variant_new("(sv)", "Active", g_variant_new("b", warn)),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      ctx->cancellable,
+                      disconnect_done,
+                      ctx);
+}
+
+static void
+deactivate_cleanup(NMModem *modem, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    /* TODO: cancel SimpleConnect() if any */
+
+    g_clear_object(&priv->ip4_config);
+
+    NM_MODEM_CLASS(nm_modem_ofono_parent_class)
+        ->deactivate_cleanup(modem, device, stop_ppp_manager);
+}
+
+static gboolean
+check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection, GError **error)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    const char *         id;
+
+    if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
+                           "connection type %s is not supported by ofono modem",
+                           nm_connection_get_connection_type(connection));
+        return FALSE;
+    }
+
+    if (!priv->imsi) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem has no IMSI");
+        return FALSE;
+    }
+
+    id = nm_connection_get_id(connection);
+
+    if (!strstr(id, "/context")) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "the connection ID has no context");
+        return FALSE;
+    }
+
+    if (!strstr(id, priv->imsi)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "the connection ID does not contain the IMSI");
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static void
+handle_sim_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (g_strcmp0(property, "SubscriberIdentity") == 0 && VARIANT_IS_OF_TYPE_STRING(v)) {
+        gsize       length;
+        const char *value_str = g_variant_get_string(v, &length);
+
+        _LOGD("SubscriberIdentify found");
+
+        /* Check for empty DBus string value */
+        if (length && g_strcmp0(value_str, "(null)") != 0
+            && g_strcmp0(value_str, priv->imsi) != 0) {
+            if (priv->imsi != NULL) {
+                _LOGW("SimManager:'SubscriberIdentity' changed: %s", priv->imsi);
+                g_free(priv->imsi);
+            }
+
+            priv->imsi = g_strdup(value_str);
+            update_modem_state(self);
+        }
+    }
+}
+
+static void
+sim_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_sim_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+sim_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->sim_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting sim properties: %s", error->message);
+        return;
+    }
+
+    _LOGD("sim v_properties is type: %s", g_variant_get_type_string(v_properties));
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+    if (!v_dict) {
+        _LOGW("error getting sim properties: no v_dict");
+        return;
+    }
+
+    _LOGD("sim v_dict is type: %s", g_variant_get_type_string(v_dict));
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_sim_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+_sim_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create SimManager proxy: %s", error->message);
+        g_clear_object(&priv->sim_proxy_cancellable);
+        return;
+    }
+
+    priv->sim_proxy = proxy;
+
+    /* Watch for custom ofono PropertyChanged signals */
+    _nm_dbus_signal_connect(priv->sim_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(sim_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->sim_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->sim_proxy_cancellable,
+                      sim_get_properties_done,
+                      self);
+}
+
+static void
+handle_sim_iface(NMModemOfono *self, gboolean found)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    _LOGD("SimManager interface %sfound", found ? "" : "not ");
+
+    if (!found && (priv->sim_proxy || priv->sim_proxy_cancellable)) {
+        _LOGI("SimManager interface disappeared");
+        nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
+        if (priv->sim_proxy) {
+            g_signal_handlers_disconnect_by_data(priv->sim_proxy, self);
+            g_clear_object(&priv->sim_proxy);
+        }
+        nm_clear_g_free(&priv->imsi);
+        update_modem_state(self);
+    } else if (found && (!priv->sim_proxy && !priv->sim_proxy_cancellable)) {
+        _LOGI("found new SimManager interface");
+
+        priv->sim_proxy_cancellable = g_cancellable_new();
+
+        g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                     | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                                 NULL, /* GDBusInterfaceInfo */
+                                 OFONO_DBUS_SERVICE,
+                                 nm_modem_get_path(NM_MODEM(self)),
+                                 OFONO_DBUS_INTERFACE_SIM_MANAGER,
+                                 priv->sim_proxy_cancellable, /* GCancellable */
+                                 _sim_proxy_new_cb,
+                                 self);
+    }
+}
+
+static void
+handle_connman_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (g_strcmp0(property, "Attached") == 0 && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean attached     = g_variant_get_boolean(v);
+        gboolean old_attached = priv->gprs_attached;
+
+        _LOGD("Attached: %s", attached ? "True" : "False");
+
+        if (priv->gprs_attached != attached) {
+            priv->gprs_attached = attached;
+
+            _LOGI("Attached %s -> %s",
+                  old_attached ? "true" : "false",
+                  attached ? "true" : "false");
+
+            update_modem_state(self);
+        }
+    }
+}
+
+static void
+connman_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_connman_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->connman_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting connman properties: %s", error->message);
+        return;
+    }
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_connman_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+_connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create ConnectionManager proxy: %s", error->message);
+        g_clear_object(&priv->connman_proxy_cancellable);
+        return;
+    }
+
+    priv->connman_proxy = proxy;
+
+    _nm_dbus_signal_connect(priv->connman_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(connman_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->connman_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->connman_proxy_cancellable,
+                      connman_get_properties_done,
+                      self);
+}
+
+static void
+handle_connman_iface(NMModemOfono *self, gboolean found)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    _LOGD("ConnectionManager interface %sfound", found ? "" : "not ");
+
+    if (!found && (priv->connman_proxy || priv->connman_proxy_cancellable)) {
+        _LOGI("ConnectionManager interface disappeared");
+        nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
+        if (priv->connman_proxy) {
+            g_signal_handlers_disconnect_by_data(priv->connman_proxy, self);
+            g_clear_object(&priv->connman_proxy);
+        }
+
+        /* The connection manager proxy disappeared, we should
+         * consider the modem disabled.
+         */
+        priv->gprs_attached = FALSE;
+
+        update_modem_state(self);
+    } else if (found && (!priv->connman_proxy && !priv->connman_proxy_cancellable)) {
+        _LOGI("found new ConnectionManager interface");
+
+        priv->connman_proxy_cancellable = g_cancellable_new();
+
+        g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                                 G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+                                     | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                                 NULL, /* GDBusInterfaceInfo */
+                                 OFONO_DBUS_SERVICE,
+                                 nm_modem_get_path(NM_MODEM(self)),
+                                 OFONO_DBUS_INTERFACE_CONNECTION_MANAGER,
+                                 priv->connman_proxy_cancellable,
+                                 _connman_proxy_new_cb,
+                                 self);
+    }
+}
+
+static void
+handle_modem_property(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if ((g_strcmp0(property, "Online") == 0) && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean online = g_variant_get_boolean(v);
+
+        _LOGD("Online: %s", online ? "True" : "False");
+
+        if (online != priv->modem_online) {
+            priv->modem_online = online;
+            _LOGI("modem is now %s", online ? "Online" : "Offline");
+            update_modem_state(self);
+        }
+
+    } else if ((g_strcmp0(property, "Interfaces") == 0) && VARIANT_IS_OF_TYPE_STRING_ARRAY(v)) {
+        const char **array, **iter;
+        gboolean     found_connman = FALSE;
+        gboolean     found_sim     = FALSE;
+
+        _LOGD("Interfaces found");
+
+        array = g_variant_get_strv(v, NULL);
+        if (array) {
+            for (iter = array; *iter; iter++) {
+                if (g_strcmp0(OFONO_DBUS_INTERFACE_SIM_MANAGER, *iter) == 0)
+                    found_sim = TRUE;
+                else if (g_strcmp0(OFONO_DBUS_INTERFACE_CONNECTION_MANAGER, *iter) == 0)
+                    found_connman = TRUE;
+            }
+            g_free(array);
+        }
+
+        handle_sim_iface(self, found_sim);
+        handle_connman_iface(self, found_connman);
+    }
+}
+
+static void
+modem_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    GVariant *v_child = g_variant_get_child_value(v, 0);
+
+    handle_modem_property(proxy, property, v_child, user_data);
+    g_variant_unref(v_child);
+}
+
+static void
+modem_get_properties_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error             = NULL;
+    gs_unref_variant GVariant *v_properties = NULL;
+    gs_unref_variant GVariant *v_dict       = NULL;
+    GVariant *                 v;
+    GVariantIter               i;
+    const char *               property;
+
+    v_properties =
+        _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, G_VARIANT_TYPE("(a{sv})"), &error);
+    if (!v_properties && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->modem_proxy_cancellable);
+
+    if (!v_properties) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("error getting modem properties: %s", error->message);
+        return;
+    }
+
+    v_dict = g_variant_get_child_value(v_properties, 0);
+    if (!v_dict) {
+        _LOGW("error getting modem properties: no v_dict");
+        return;
+    }
+
+    /*
+     * TODO:
+     * 1) optimize by looking up properties ( Online, Interfaces ), instead
+     *    of iterating
+     *
+     * 2) reduce code duplication between all of the get_properties_done
+     *    functions in this class.
+     */
+
+    g_variant_iter_init(&i, v_dict);
+    while (g_variant_iter_next(&i, "{&sv}", &property, &v)) {
+        handle_modem_property(NULL, property, v, self);
+        g_variant_unref(v);
+    }
+}
+
+static void
+stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error  = NULL;
+    gs_unref_variant GVariant *v = NULL;
+
+    v = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), result, &error);
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_clear_object(&priv->context_proxy_cancellable);
+
+    nm_clear_pointer(&priv->connect_properties, g_hash_table_destroy);
+
+    if (error) {
+        _LOGW("connection failed: %s", error->message);
+
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        /*
+         * FIXME: add code to check for InProgress so that the
+         * connection doesn't continue to try and activate,
+         * leading to the connection being disabled, and a 5m
+         * timeout...
+         */
+    }
+}
+
+static void
+context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMPlatformIP4Address addr;
+    gboolean             ret          = FALSE;
+    gs_unref_variant GVariant *v_dict = NULL;
+    const char *               interface;
+    const char *               s;
+    const char **              array, **iter;
+    guint32                    address_network, gateway_network;
+    guint32                    ip4_route_table, ip4_route_metric;
+    int                        ifindex;
+    GError *                   error = NULL;
+
+    _LOGD("PropertyChanged: %s", property);
+
+    /*
+     * TODO: might be a good idea and re-factor this to mimic bluez-device,
+     * ie. have this function just check the key, and call a sub-func to
+     * handle the action.
+     */
+
+    if (g_strcmp0(property, "Settings") != 0)
+        return;
+
+    v_dict = g_variant_get_child_value(v, 0);
+    if (!v_dict) {
+        _LOGW("error getting IPv4 Settings: no v_dict");
+        goto out;
+    }
+
+    _LOGI("IPv4 static Settings:");
+
+    if (!g_variant_lookup(v_dict, "Interface", "&s", &interface)) {
+        _LOGW("Settings 'Interface' missing");
+        goto out;
+    }
+
+    _LOGD("Interface: %s", interface);
+    if (!nm_modem_set_data_port(NM_MODEM(self),
+                                NM_PLATFORM_GET,
+                                interface,
+                                NM_MODEM_IP_METHOD_STATIC,
+                                NM_MODEM_IP_METHOD_UNKNOWN,
+                                0,
+                                &error)) {
+        _LOGW("failed to connect to modem: %s", error->message);
+        g_clear_error(&error);
+        goto out;
+    }
+
+    ifindex = nm_modem_get_ip_ifindex(NM_MODEM(self));
+    nm_assert(ifindex > 0);
+
+    /* TODO: verify handling of ip4_config; check other places it's used... */
+    g_clear_object(&priv->ip4_config);
+
+    priv->ip4_config = nm_ip4_config_new(nm_platform_get_multi_idx(NM_PLATFORM_GET), ifindex);
+
+    if (!g_variant_lookup(v_dict, "Address", "&s", &s)) {
+        _LOGW("Settings 'Address' missing");
+        goto out;
+    }
+    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+        _LOGW("can't convert 'Address' %s to addr", s ?: "");
+        goto out;
+    }
+    memset(&addr, 0, sizeof(addr));
+    addr.ifindex     = ifindex;
+    addr.address     = address_network;
+    addr.addr_source = NM_IP_CONFIG_SOURCE_WWAN;
+
+    if (!g_variant_lookup(v_dict, "Netmask", "&s", &s)) {
+        _LOGW("Settings 'Netmask' missing");
+        goto out;
+    }
+    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+        _LOGW("invalid 'Netmask': %s", s ?: "");
+        goto out;
+    }
+    addr.plen = nm_utils_ip4_netmask_to_prefix(address_network);
+
+    _LOGI("Address: %s", nm_platform_ip4_address_to_string(&addr, NULL, 0));
+    nm_ip4_config_add_address(priv->ip4_config, &addr);
+
+    if (!g_variant_lookup(v_dict, "Gateway", "&s", &s) || !s) {
+        _LOGW("Settings 'Gateway' missing");
+        goto out;
+    }
+    if (!nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &gateway_network)) {
+        _LOGW("invalid 'Gateway': %s", s);
+        goto out;
+    }
+    nm_modem_get_route_parameters(NM_MODEM(self), &ip4_route_table, &ip4_route_metric, NULL, NULL);
+    {
+        const NMPlatformIP4Route r = {
+            .rt_source     = NM_IP_CONFIG_SOURCE_WWAN,
+            .gateway       = gateway_network,
+            .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+            .metric        = ip4_route_metric,
+        };
+
+        _LOGI("Gateway: %s", s);
+        nm_ip4_config_add_route(priv->ip4_config, &r, NULL);
+    }
+
+    if (!g_variant_lookup(v_dict, "DomainNameServers", "^a&s", &array)) {
+        _LOGW("Settings 'DomainNameServers' missing");
+        goto out;
+    }
+    if (array) {
+        for (iter = array; *iter; iter++) {
+            if (nm_utils_parse_inaddr_bin(AF_INET, *iter, NULL, &address_network)
+                && address_network) {
+                _LOGI("DNS: %s", *iter);
+                nm_ip4_config_add_nameserver(priv->ip4_config, address_network);
+            } else {
+                _LOGW("invalid NameServer: %s", *iter);
+            }
+        }
+
+        if (iter == array) {
+            _LOGW("Settings: 'DomainNameServers': none specified");
+            g_free(array);
+            goto out;
+        }
+        g_free(array);
+    }
+
+    if (g_variant_lookup(v_dict, "MessageProxy", "&s", &s)) {
+        _LOGI("MessageProxy: %s", s);
+        if (s && nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+            nm_modem_get_route_parameters(NM_MODEM(self),
+                                          &ip4_route_table,
+                                          &ip4_route_metric,
+                                          NULL,
+                                          NULL);
+
+            {
+                const NMPlatformIP4Route mms_route = {
+                    .network       = address_network,
+                    .plen          = 32,
+                    .gateway       = gateway_network,
+                    .table_coerced = nm_platform_route_table_coerce(ip4_route_table),
+                    .metric        = ip4_route_metric,
+                };
+
+                nm_ip4_config_add_route(priv->ip4_config, &mms_route, NULL);
+            }
+        } else {
+            _LOGW("invalid MessageProxy: %s", s);
+        }
+    }
+
+    ret = TRUE;
+
+out:
+    if (nm_modem_get_state(NM_MODEM(self)) != NM_MODEM_STATE_CONNECTED) {
+        _LOGI("emitting PREPARE_RESULT: %s", ret ? "TRUE" : "FALSE");
+        nm_modem_emit_prepare_result(NM_MODEM(self),
+                                     ret,
+                                     ret ? NM_DEVICE_STATE_REASON_NONE
+                                         : NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    } else {
+        _LOGW("MODEM_PPP_FAILED");
+        nm_modem_emit_ppp_failed(NM_MODEM(self), NM_DEVICE_STATE_REASON_PPP_FAILED);
+    }
+}
+
+static NMActStageReturn
+static_stage3_ip4_config_start(NMModem *            modem,
+                               NMActRequest *       req,
+                               NMDeviceStateReason *out_failure_reason)
+{
+    NMModemOfono *       self  = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv  = NM_MODEM_OFONO_GET_PRIVATE(self);
+    GError *             error = NULL;
+
+    if (!priv->ip4_config) {
+        _LOGD("IP4 config not ready(?)");
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    _LOGD("IP4 config is done; setting modem_state -> CONNECTED");
+    g_signal_emit_by_name(self, NM_MODEM_IP4_CONFIG_RESULT, priv->ip4_config, error);
+
+    /* Signal listener takes ownership of the IP4Config */
+    priv->ip4_config = NULL;
+
+    nm_modem_set_state(NM_MODEM(self),
+                       NM_MODEM_STATE_CONNECTED,
+                       nm_modem_state_to_string(NM_MODEM_STATE_CONNECTED));
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy || g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGE("failed to create ofono ConnectionContext DBus proxy: %s", error->message);
+        g_clear_object(&priv->context_proxy_cancellable);
+        nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        return;
+    }
+
+    priv->context_proxy = proxy;
+
+    if (!priv->gprs_attached) {
+        g_clear_object(&priv->context_proxy_cancellable);
+        nm_modem_emit_prepare_result(NM_MODEM(self),
+                                     FALSE,
+                                     NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return;
+    }
+
+    /* We have an old copy of the settings from a previous activation,
+     * clear it so that we can gate getting the IP config from oFono
+     * on whether or not we have already received them
+     */
+    g_clear_object(&priv->ip4_config);
+
+    _nm_dbus_signal_connect(priv->context_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(context_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->context_proxy,
+                      "SetProperty",
+                      g_variant_new("(sv)", "Active", g_variant_new("b", TRUE)),
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->context_proxy_cancellable,
+                      stage1_prepare_done,
+                      self);
+}
+
+static void
+do_context_activate(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    g_return_if_fail(NM_IS_MODEM_OFONO(self));
+
+    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
+    g_clear_object(&priv->context_proxy);
+
+    priv->context_proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             OFONO_DBUS_SERVICE,
+                             priv->context_path,
+                             OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT,
+                             priv->context_proxy_cancellable,
+                             context_proxy_new_cb,
+                             self);
+}
+
+static GHashTable *
+create_connect_properties(NMConnection *connection)
+{
+    NMSettingGsm *setting;
+    GHashTable *  properties;
+    const char *  str;
+
+    setting    = nm_connection_get_setting_gsm(connection);
+    properties = g_hash_table_new(nm_str_hash, g_str_equal);
+
+    str = nm_setting_gsm_get_apn(setting);
+    if (str)
+        g_hash_table_insert(properties, "AccessPointName", g_strdup(str));
+
+    str = nm_setting_gsm_get_username(setting);
+    if (str)
+        g_hash_table_insert(properties, "Username", g_strdup(str));
+
+    str = nm_setting_gsm_get_password(setting);
+    if (str)
+        g_hash_table_insert(properties, "Password", g_strdup(str));
+
+    return properties;
+}
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            modem,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(modem);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    const char *         context_id;
+    char **              id = NULL;
+
+    context_id = nm_connection_get_id(connection);
+    id         = g_strsplit(context_id, "/", 0);
+    g_return_val_if_fail(id[2], NM_ACT_STAGE_RETURN_FAILURE);
+
+    _LOGD("trying %s %s", id[1], id[2]);
+
+    g_free(priv->context_path);
+    priv->context_path = g_strdup_printf("%s/%s", nm_modem_get_path(modem), id[2]);
+    g_strfreev(id);
+
+    if (!priv->context_path) {
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (priv->connect_properties)
+        g_hash_table_destroy(priv->connect_properties);
+
+    priv->connect_properties = create_connect_properties(connection);
+
+    _LOGI("activating context %s", priv->context_path);
+
+    if (nm_modem_get_state(modem) == NM_MODEM_STATE_REGISTERED) {
+        do_context_activate(self);
+    } else {
+        _LOGW("could not activate context: modem is not registered.");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+static void
+modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono *       self;
+    NMModemOfonoPrivate *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy *          proxy;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = NM_MODEM_OFONO(user_data);
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGE("failed to create ofono modem DBus proxy: %s", error->message);
+        g_clear_object(&priv->modem_proxy_cancellable);
+        return;
+    }
+
+    priv->modem_proxy = proxy;
+
+    _nm_dbus_signal_connect(priv->modem_proxy,
+                            "PropertyChanged",
+                            G_VARIANT_TYPE("(sv)"),
+                            G_CALLBACK(modem_property_changed),
+                            self);
+
+    g_dbus_proxy_call(priv->modem_proxy,
+                      "GetProperties",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->modem_proxy_cancellable,
+                      modem_get_properties_done,
+                      self);
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_ofono_init(NMModemOfono *self)
+{}
+
+static void
+constructed(GObject *object)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(object);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    priv->modem_proxy_cancellable = g_cancellable_new();
+
+    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+                             G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+                             NULL,
+                             OFONO_DBUS_SERVICE,
+                             nm_modem_get_path(NM_MODEM(self)),
+                             OFONO_DBUS_INTERFACE_MODEM,
+                             priv->modem_proxy_cancellable,
+                             modem_proxy_new_cb,
+                             self);
+
+    G_OBJECT_CLASS(nm_modem_ofono_parent_class)->constructed(object);
+}
+
+NMModem *
+nm_modem_ofono_new(const char *path)
+{
+    gs_free char *basename = NULL;
+
+    g_return_val_if_fail(path != NULL, NULL);
+
+    nm_log_info(LOGD_MB, "ofono: creating new Ofono modem path %s", path);
+
+    /* Use short modem name (not its object path) as the NM device name (which
+     * comes from NM_MODEM_UID)and the device ID.
+     */
+    basename = g_path_get_basename(path);
+
+    return g_object_new(NM_TYPE_MODEM_OFONO,
+                        NM_MODEM_PATH,
+                        path,
+                        NM_MODEM_UID,
+                        basename,
+                        NM_MODEM_DEVICE_ID,
+                        basename,
+                        NM_MODEM_CONTROL_PORT,
+                        "ofono", /* mandatory */
+                        NM_MODEM_DRIVER,
+                        "ofono",
+                        NM_MODEM_STATE,
+                        (int) NM_MODEM_STATE_INITIALIZING,
+                        NULL);
+}
+
+static void
+dispose(GObject *object)
+{
+    NMModemOfono *       self = NM_MODEM_OFONO(object);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    nm_clear_g_cancellable(&priv->modem_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->context_proxy_cancellable);
+    nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
+
+    if (priv->connect_properties) {
+        g_hash_table_destroy(priv->connect_properties);
+        priv->connect_properties = NULL;
+    }
+
+    g_clear_object(&priv->ip4_config);
+
+    if (priv->modem_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->modem_proxy, self);
+        g_clear_object(&priv->modem_proxy);
+    }
+
+    if (priv->connman_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->connman_proxy, self);
+        g_clear_object(&priv->connman_proxy);
+    }
+
+    if (priv->context_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->context_proxy, self);
+        g_clear_object(&priv->context_proxy);
+    }
+
+    if (priv->sim_proxy) {
+        g_signal_handlers_disconnect_by_data(priv->sim_proxy, self);
+        g_clear_object(&priv->sim_proxy);
+    }
+
+    g_free(priv->imsi);
+    priv->imsi = NULL;
+
+    G_OBJECT_CLASS(nm_modem_ofono_parent_class)->dispose(object);
+}
+
+static void
+nm_modem_ofono_class_init(NMModemOfonoClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+    NMModemClass *modem_class  = NM_MODEM_CLASS(klass);
+
+    object_class->constructed = constructed;
+    object_class->dispose     = dispose;
+
+    modem_class->get_capabilities                       = get_capabilities;
+    modem_class->disconnect                             = disconnect;
+    modem_class->deactivate_cleanup                     = deactivate_cleanup;
+    modem_class->check_connection_compatible_with_modem = check_connection_compatible_with_modem;
+
+    modem_class->modem_act_stage1_prepare       = modem_act_stage1_prepare;
+    modem_class->static_stage3_ip4_config_start = static_stage3_ip4_config_start;
+}
diff --git a/src/core/devices/wwan/nm-modem-ofono.h b/src/core/devices/wwan/nm-modem-ofono.h
new file mode 100644
index 00000000..260e3954
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem-ofono.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2013 - Canonical Ltd.
+ */
+
+#ifndef NM_MODEM_OFONO_H
+#define NM_MODEM_OFONO_H
+
+#include "nm-modem.h"
+
+#define NM_TYPE_MODEM_OFONO    (nm_modem_ofono_get_type())
+#define NM_MODEM_OFONO(obj)    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM_OFONO, NMModemOfono))
+#define NM_IS_MODEM_OFONO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM_OFONO))
+#define NM_MODEM_OFONO_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM_OFONO, NMModemOfonoClass))
+#define NM_IS_MODEM_OFONO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM_OFONO))
+#define NM_MODEM_OFONO_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM_OFONO, NMModemOfonoClass))
+
+#define OFONO_DBUS_SERVICE                      "org.ofono"
+#define OFONO_DBUS_PATH                         "/"
+#define OFONO_DBUS_INTERFACE                    "org.ofono.Manager"
+#define OFONO_DBUS_INTERFACE_MODEM              "org.ofono.Modem"
+#define OFONO_DBUS_INTERFACE_CONNECTION_MANAGER "org.ofono.ConnectionManager"
+#define OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT "org.ofono.ConnectionContext"
+#define OFONO_DBUS_INTERFACE_SIM_MANAGER        "org.ofono.SimManager"
+
+typedef struct _NMModemOfono      NMModemOfono;
+typedef struct _NMModemOfonoClass NMModemOfonoClass;
+
+GType nm_modem_ofono_get_type(void);
+
+NMModem *nm_modem_ofono_new(const char *path);
+
+#endif /* NM_MODEM_OFONO_H */
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
new file mode 100644
index 00000000..0d334fa4
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem.c
@@ -0,0 +1,2037 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2014 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-modem.h"
+
+#include <fcntl.h>
+#include <termios.h>
+#include <linux/rtnetlink.h>
+
+#include "nm-core-internal.h"
+#include "platform/nm-platform.h"
+#include "nm-setting-connection.h"
+#include "NetworkManagerUtils.h"
+#include "devices/nm-device-private.h"
+#include "nm-netns.h"
+#include "nm-act-request.h"
+#include "nm-ip4-config.h"
+#include "nm-ip6-config.h"
+#include "ppp/nm-ppp-manager-call.h"
+#include "ppp/nm-ppp-status.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMModem,
+                             PROP_CONTROL_PORT,
+                             PROP_IP_IFINDEX,
+                             PROP_PATH,
+                             PROP_UID,
+                             PROP_DRIVER,
+                             PROP_STATE,
+                             PROP_DEVICE_ID,
+                             PROP_SIM_ID,
+                             PROP_IP_TYPES,
+                             PROP_SIM_OPERATOR_ID,
+                             PROP_OPERATOR_CODE,
+                             PROP_APN, );
+
+enum {
+    PPP_STATS,
+    PPP_FAILED,
+    PREPARE_RESULT,
+    IP4_CONFIG_RESULT,
+    IP6_CONFIG_RESULT,
+    AUTH_REQUESTED,
+    AUTH_RESULT,
+    REMOVED,
+    STATE_CHANGED,
+    LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+typedef struct _NMModemPrivate {
+    char *uid;
+    char *path;
+    char *driver;
+    char *control_port;
+    char *data_port;
+
+    /* TODO: ip_iface is solely used for nm_modem_owns_port().
+     * We should rework the code that it's not necessary */
+    char *ip_iface;
+
+    int                ip_ifindex;
+    NMModemIPMethod    ip4_method;
+    NMModemIPMethod    ip6_method;
+    NMUtilsIPv6IfaceId iid;
+    NMModemState       state;
+    NMModemState       prev_state; /* revert to this state if enable/disable fails */
+    char *             device_id;
+    char *             sim_id;
+    NMModemIPType      ip_types;
+    char *             sim_operator_id;
+    char *             operator_code;
+    char *             apn;
+
+    NMPPPManager *ppp_manager;
+
+    NMActRequest *                act_request;
+    guint32                       secrets_tries;
+    NMActRequestGetSecretsCallId *secrets_id;
+
+    guint mm_ip_timeout;
+
+    guint32 ip4_route_table;
+    guint32 ip4_route_metric;
+    guint32 ip6_route_table;
+    guint32 ip6_route_metric;
+
+    /* PPP stats */
+    guint32 in_bytes;
+    guint32 out_bytes;
+
+    bool claimed : 1;
+} NMModemPrivate;
+
+G_DEFINE_TYPE(NMModem, nm_modem, G_TYPE_OBJECT)
+
+#define NM_MODEM_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMModem, NM_IS_MODEM)
+
+/*****************************************************************************/
+
+#define _NMLOG_PREFIX_BUFLEN 64
+#define _NMLOG_PREFIX_NAME   "modem"
+#define _NMLOG_DOMAIN        LOGD_MB
+
+static const char *
+_nmlog_prefix(char *prefix, NMModem *self)
+{
+    const char *uuid;
+    int         c;
+
+    if (!self)
+        return "";
+
+    uuid = nm_modem_get_uid(self);
+
+    if (uuid) {
+        char pp[_NMLOG_PREFIX_BUFLEN - 5];
+
+        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%s]", nm_strquote(pp, sizeof(pp), uuid));
+    } else
+        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%p]", self);
+    nm_assert(c < _NMLOG_PREFIX_BUFLEN);
+
+    return prefix;
+}
+
+#define _NMLOG(level, ...)                                                        \
+    G_STMT_START                                                                  \
+    {                                                                             \
+        char _prefix[_NMLOG_PREFIX_BUFLEN];                                       \
+                                                                                  \
+        nm_log((level),                                                           \
+               _NMLOG_DOMAIN,                                                     \
+               NULL,                                                              \
+               NULL,                                                              \
+               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                       \
+               _NMLOG_PREFIX_NAME,                                                \
+               _nmlog_prefix(_prefix, (self)) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+    }                                                                             \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void _set_ip_ifindex(NMModem *self, int ifindex, const char *ifname);
+
+/*****************************************************************************/
+/* State/enabled/connected */
+
+static const char *state_table[] = {
+    [NM_MODEM_STATE_UNKNOWN]       = "unknown",
+    [NM_MODEM_STATE_FAILED]        = "failed",
+    [NM_MODEM_STATE_INITIALIZING]  = "initializing",
+    [NM_MODEM_STATE_LOCKED]        = "locked",
+    [NM_MODEM_STATE_DISABLED]      = "disabled",
+    [NM_MODEM_STATE_DISABLING]     = "disabling",
+    [NM_MODEM_STATE_ENABLING]      = "enabling",
+    [NM_MODEM_STATE_ENABLED]       = "enabled",
+    [NM_MODEM_STATE_SEARCHING]     = "searching",
+    [NM_MODEM_STATE_REGISTERED]    = "registered",
+    [NM_MODEM_STATE_DISCONNECTING] = "disconnecting",
+    [NM_MODEM_STATE_CONNECTING]    = "connecting",
+    [NM_MODEM_STATE_CONNECTED]     = "connected",
+};
+
+const char *
+nm_modem_state_to_string(NMModemState state)
+{
+    if ((gsize) state < G_N_ELEMENTS(state_table))
+        return state_table[state];
+    return NULL;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_is_claimed(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+
+    return NM_MODEM_GET_PRIVATE(self)->claimed;
+}
+
+NMModem *
+nm_modem_claim(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_val_if_fail(!priv->claimed, NULL);
+
+    priv->claimed = TRUE;
+    return g_object_ref(self);
+}
+
+void
+nm_modem_unclaim(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(priv->claimed);
+
+    /* we don't actually unclaim the instance. This instance should not be re-used
+     * by another owner, that is because we only claim modems as we receive them.
+     * There is no mechanism that somebody else would later re-use them again.
+     *
+     * // priv->claimed = FALSE; */
+
+    g_object_unref(self);
+}
+
+/*****************************************************************************/
+
+NMModemState
+nm_modem_get_state(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->state;
+}
+
+void
+nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason)
+{
+    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
+    NMModemState    old_state = priv->state;
+
+    priv->prev_state = NM_MODEM_STATE_UNKNOWN;
+
+    if (new_state != old_state) {
+        _LOGI("modem state changed, '%s' --> '%s' (reason: %s)",
+              nm_modem_state_to_string(old_state),
+              nm_modem_state_to_string(new_state),
+              reason ?: "none");
+
+        priv->state = new_state;
+        _notify(self, PROP_STATE);
+        g_signal_emit(self, signals[STATE_CHANGED], 0, (int) new_state, (int) old_state);
+    }
+}
+
+void
+nm_modem_set_prev_state(NMModem *self, const char *reason)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* Reset modem to previous state if the state hasn't already changed */
+    if (priv->prev_state != NM_MODEM_STATE_UNKNOWN)
+        nm_modem_set_state(self, priv->prev_state, reason);
+}
+
+void
+nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
+{
+    NMModemPrivate *priv       = NM_MODEM_GET_PRIVATE(self);
+    NMModemState    prev_state = priv->state;
+
+    if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) {
+        _LOGD("cannot enable modem: already enabled");
+        return;
+    }
+    if (!enabled && priv->state <= NM_MODEM_STATE_DISABLING) {
+        _LOGD("cannot disable modem: already disabled");
+        return;
+    }
+
+    if (priv->state <= NM_MODEM_STATE_INITIALIZING) {
+        _LOGD("cannot enable/disable modem: initializing or failed");
+        return;
+    } else if (priv->state == NM_MODEM_STATE_LOCKED) {
+        /* Don't try to enable if the modem is locked since that will fail */
+        _LOGW("cannot enable/disable modem: locked");
+
+        /* Try to unlock the modem if it's being enabled */
+        if (enabled)
+            g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+        return;
+    }
+
+    /* Not all modem classes support set_mm_enabled */
+    if (NM_MODEM_GET_CLASS(self)->set_mm_enabled)
+        NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);
+
+    /* Pre-empt the state change signal */
+    nm_modem_set_state(self,
+                       enabled ? NM_MODEM_STATE_ENABLING : NM_MODEM_STATE_DISABLING,
+                       "user preference");
+    priv->prev_state = prev_state;
+}
+
+void
+nm_modem_emit_removed(NMModem *self)
+{
+    g_signal_emit(self, signals[REMOVED], 0);
+}
+
+void
+nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason)
+{
+    nm_assert(NM_IS_MODEM(self));
+
+    g_signal_emit(self, signals[PREPARE_RESULT], 0, success, (guint) reason);
+}
+
+void
+nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason)
+{
+    nm_assert(NM_IS_MODEM(self));
+
+    g_signal_emit(self, signals[PPP_FAILED], 0, (guint) reason);
+}
+
+NMModemIPType
+nm_modem_get_supported_ip_types(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->ip_types;
+}
+
+const char *
+nm_modem_ip_type_to_string(NMModemIPType ip_type)
+{
+    switch (ip_type) {
+    case NM_MODEM_IP_TYPE_IPV4:
+        return "ipv4";
+    case NM_MODEM_IP_TYPE_IPV6:
+        return "ipv6";
+    case NM_MODEM_IP_TYPE_IPV4V6:
+        return "ipv4v6";
+    default:
+        g_return_val_if_reached("unknown");
+    }
+}
+
+static GArray *
+build_single_ip_type_array(NMModemIPType type)
+{
+    return g_array_append_val(g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 1), type);
+}
+
+/**
+ * nm_modem_get_connection_ip_type:
+ * @self: the #NMModem
+ * @connection: the #NMConnection to determine IP type to use
+ *
+ * Given a modem and a connection, determine which #NMModemIPTypes to use
+ * when connecting.
+ *
+ * Returns: an array of #NMModemIpType values, in the order in which they
+ * should be tried.
+ */
+GArray *
+nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error)
+{
+    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
+    NMSettingIPConfig *s_ip4, *s_ip6;
+    const char *       method;
+    gboolean           ip4 = TRUE, ip6 = TRUE;
+    gboolean           ip4_may_fail = TRUE, ip6_may_fail = TRUE;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(connection);
+    if (s_ip4) {
+        method = nm_setting_ip_config_get_method(s_ip4);
+        if (g_strcmp0(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) == 0)
+            ip4 = FALSE;
+        ip4_may_fail = nm_setting_ip_config_get_may_fail(s_ip4);
+    }
+
+    s_ip6 = nm_connection_get_setting_ip6_config(connection);
+    if (s_ip6) {
+        method = nm_setting_ip_config_get_method(s_ip6);
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
+            ip6 = FALSE;
+        ip6_may_fail = nm_setting_ip_config_get_may_fail(s_ip6);
+    }
+
+    if (ip4 && !ip6) {
+        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV4)) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                                "Connection requested IPv4 but IPv4 is "
+                                "unsupported by the modem.");
+            return NULL;
+        }
+        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV4);
+    }
+
+    if (ip6 && !ip4) {
+        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV6)) {
+            g_set_error_literal(error,
+                                NM_DEVICE_ERROR,
+                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                                "Connection requested IPv6 but IPv6 is "
+                                "unsupported by the modem.");
+            return NULL;
+        }
+        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV6);
+    }
+
+    if (ip4 && ip6) {
+        NMModemIPType type;
+        GArray *      out;
+
+        out = g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 3);
+
+        /* Modem supports dual-stack? */
+        if (priv->ip_types & NM_MODEM_IP_TYPE_IPV4V6) {
+            type = NM_MODEM_IP_TYPE_IPV4V6;
+            g_array_append_val(out, type);
+        }
+
+        /* If IPv6 may-fail=false, we should NOT try IPv4 as fallback */
+        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV4) && ip6_may_fail) {
+            type = NM_MODEM_IP_TYPE_IPV4;
+            g_array_append_val(out, type);
+        }
+
+        /* If IPv4 may-fail=false, we should NOT try IPv6 as fallback */
+        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV6) && ip4_may_fail) {
+            type = NM_MODEM_IP_TYPE_IPV6;
+            g_array_append_val(out, type);
+        }
+
+        if (out->len > 0)
+            return out;
+
+        /* Error... */
+        g_array_unref(out);
+        g_set_error_literal(error,
+                            NM_DEVICE_ERROR,
+                            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                            "Connection requested both IPv4 and IPv6 "
+                            "but dual-stack addressing is unsupported "
+                            "by the modem.");
+        return NULL;
+    }
+
+    g_set_error_literal(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
+                        "Connection specified no IP configuration!");
+    return NULL;
+}
+
+const char *
+nm_modem_get_device_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->device_id;
+}
+
+const char *
+nm_modem_get_sim_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->sim_id;
+}
+
+const char *
+nm_modem_get_sim_operator_id(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->sim_operator_id;
+}
+
+const char *
+nm_modem_get_operator_code(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->operator_code;
+}
+
+const char *
+nm_modem_get_apn(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->apn;
+}
+
+/*****************************************************************************/
+/* IP method PPP */
+
+static void
+ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
+{
+    switch (status) {
+    case NM_PPP_STATUS_DISCONNECT:
+        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
+        break;
+    case NM_PPP_STATUS_DEAD:
+        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_FAILED);
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+
+    nm_assert(ifindex >= 0);
+    nm_assert(NM_MODEM_GET_PRIVATE(self)->ppp_manager == ppp_manager);
+
+    if (ifindex <= 0 && iface) {
+        /* this might happen, if the ifname was already deleted
+         * and we failed to resolve ifindex.
+         *
+         * Forget about the name. */
+        iface = NULL;
+    }
+    _set_ip_ifindex(self, ifindex, iface);
+}
+
+static void
+ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+    guint32  i, num;
+    guint32  bad_dns1       = htonl(0x0A0B0C0D);
+    guint32  good_dns1      = htonl(0x04020201); /* GTE nameserver */
+    guint32  bad_dns2       = htonl(0x0A0B0C0E);
+    guint32  good_dns2      = htonl(0x04020202); /* GTE nameserver */
+    gboolean dns_workaround = FALSE;
+
+    /* Work around a PPP bug (#1732) which causes many mobile broadband
+     * providers to return 10.11.12.13 and 10.11.12.14 for the DNS servers.
+     * Apparently fixed in ppp-2.4.5 but we've had some reports that this is
+     * not the case.
+     *
+     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=2e09ef6886bbf00bc5a9a641110f801e372ffde6
+     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=f8191bf07df374f119a07910a79217c7618f113e
+     */
+
+    num = nm_ip4_config_get_num_nameservers(config);
+    if (num == 2) {
+        gboolean found1 = FALSE, found2 = FALSE;
+
+        for (i = 0; i < num; i++) {
+            guint32 ns = nm_ip4_config_get_nameserver(config, i);
+
+            if (ns == bad_dns1)
+                found1 = TRUE;
+            else if (ns == bad_dns2)
+                found2 = TRUE;
+        }
+
+        /* Be somewhat conservative about substitutions; the "bad" nameservers
+         * could actually be valid in some cases, so only substitute if ppp
+         * returns *only* the two bad nameservers.
+         */
+        dns_workaround = (found1 && found2);
+    }
+
+    if (!num || dns_workaround) {
+        _LOGW("compensating for invalid PPP-provided nameservers");
+        nm_ip4_config_reset_nameservers(config);
+        nm_ip4_config_add_nameserver(config, good_dns1);
+        nm_ip4_config_add_nameserver(config, good_dns2);
+    }
+
+    g_signal_emit(self, signals[IP4_CONFIG_RESULT], 0, config, NULL);
+}
+
+static void
+ppp_ip6_config(NMPPPManager *            ppp_manager,
+               const NMUtilsIPv6IfaceId *iid,
+               NMIP6Config *             config,
+               gpointer                  user_data)
+{
+    NMModem *self = NM_MODEM(user_data);
+
+    NM_MODEM_GET_PRIVATE(self)->iid = *iid;
+
+    nm_modem_emit_ip6_config_result(self, config, NULL);
+}
+
+static void
+ppp_stats(NMPPPManager *ppp_manager, guint i_in_bytes, guint i_out_bytes, gpointer user_data)
+{
+    NMModem *       self      = NM_MODEM(user_data);
+    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
+    guint32         in_bytes  = i_in_bytes;
+    guint32         out_bytes = i_out_bytes;
+
+    if (priv->in_bytes != in_bytes || priv->out_bytes != out_bytes) {
+        priv->in_bytes  = in_bytes;
+        priv->out_bytes = out_bytes;
+        g_signal_emit(self, signals[PPP_STATS], 0, (guint) in_bytes, (guint) out_bytes);
+    }
+}
+
+static gboolean
+port_speed_is_zero(const char *port)
+{
+    struct termios    options;
+    nm_auto_close int fd   = -1;
+    gs_free char *    path = NULL;
+
+    nm_assert(port);
+
+    if (port[0] != '/') {
+        if (!port[0] || strchr(port, '/') || NM_IN_STRSET(port, ".", ".."))
+            return FALSE;
+        path = g_build_path("/sys/class/tty", port, NULL);
+        port = path;
+    }
+
+    fd = open(port, O_RDWR | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
+    if (fd < 0)
+        return FALSE;
+
+    memset(&options, 0, sizeof(struct termios));
+    if (tcgetattr(fd, &options) != 0)
+        return FALSE;
+
+    return cfgetospeed(&options) == B0;
+}
+
+static NMActStageReturn
+ppp_stage3_ip_config_start(NMModem *            self,
+                           NMActRequest *       req,
+                           NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate *priv          = NM_MODEM_GET_PRIVATE(self);
+    const char *    ppp_name      = NULL;
+    GError *        error         = NULL;
+    guint           ip_timeout    = 30;
+    guint           baud_override = 0;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);
+
+    /* If we're already running PPP don't restart it; for example, if both
+     * IPv4 and IPv6 are requested, IPv4 gets started first, but we use the
+     * same pppd for both v4 and v6.
+     */
+    if (priv->ppp_manager)
+        return NM_ACT_STAGE_RETURN_POSTPONE;
+
+    if (NM_MODEM_GET_CLASS(self)->get_user_pass) {
+        NMConnection *connection = nm_act_request_get_applied_connection(req);
+
+        g_assert(connection);
+        if (!NM_MODEM_GET_CLASS(self)->get_user_pass(self, connection, &ppp_name, NULL))
+            return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    if (!priv->data_port) {
+        _LOGE("error starting PPP (no data port)");
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    /* Check if ModemManager requested a specific IP timeout to be used. If 0 reported,
+     * use the default one (30s) */
+    if (priv->mm_ip_timeout > 0) {
+        _LOGI("using modem-specified IP timeout: %u seconds", priv->mm_ip_timeout);
+        ip_timeout = priv->mm_ip_timeout;
+    }
+
+    /* Some tty drivers and modems ignore port speed, but pppd requires the
+     * port speed to be > 0 or it exits. If the port speed is 0 pass an
+     * explicit speed to pppd to prevent the exit.
+     * https://bugzilla.redhat.com/show_bug.cgi?id=1281731
+     */
+    if (port_speed_is_zero(priv->data_port))
+        baud_override = 57600;
+
+    priv->ppp_manager = nm_ppp_manager_create(priv->data_port, &error);
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            priv->ip4_route_table,
+                                            priv->ip4_route_metric,
+                                            priv->ip6_route_table,
+                                            priv->ip6_route_metric);
+    }
+
+    if (!priv->ppp_manager
+        || !nm_ppp_manager_start(priv->ppp_manager,
+                                 req,
+                                 ppp_name,
+                                 ip_timeout,
+                                 baud_override,
+                                 &error)) {
+        _LOGE("error starting PPP: %s", error->message);
+        g_error_free(error);
+        g_clear_object(&priv->ppp_manager);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
+                     G_CALLBACK(ppp_state_changed),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
+                     G_CALLBACK(ppp_ifindex_set),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
+                     G_CALLBACK(ppp_ip4_config),
+                     self);
+    g_signal_connect(priv->ppp_manager,
+                     NM_PPP_MANAGER_SIGNAL_IP6_CONFIG,
+                     G_CALLBACK(ppp_ip6_config),
+                     self);
+    g_signal_connect(priv->ppp_manager, NM_PPP_MANAGER_SIGNAL_STATS, G_CALLBACK(ppp_stats), self);
+
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+NMActStageReturn
+nm_modem_stage3_ip4_config_start(NMModem *            self,
+                                 NMDevice *           device,
+                                 NMDeviceClass *      device_class,
+                                 NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate * priv;
+    NMActRequest *   req;
+    NMConnection *   connection;
+    const char *     method;
+    NMActStageReturn ret;
+
+    _LOGD("ip4_config_start");
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_DEVICE(device), NM_ACT_STAGE_RETURN_FAILURE);
+    g_return_val_if_fail(NM_IS_DEVICE_CLASS(device_class), NM_ACT_STAGE_RETURN_FAILURE);
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_modem_set_route_parameters_from_device(self, device);
+
+    method = nm_utils_get_ip_config_method(connection, AF_INET);
+
+    /* Only Disabled and Auto methods make sense for WWAN */
+    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    if (!nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        _LOGE("unhandled WWAN IPv4 method '%s'; will fail", method);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    switch (priv->ip4_method) {
+    case NM_MODEM_IP_METHOD_PPP:
+        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_STATIC:
+        _LOGD("MODEM_IP_METHOD_STATIC");
+        ret =
+            NM_MODEM_GET_CLASS(self)->static_stage3_ip4_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_AUTO:
+        _LOGD("MODEM_IP_METHOD_AUTO");
+        ret = device_class->act_stage3_ip_config_start(device, AF_INET, NULL, out_failure_reason);
+        break;
+    default:
+        _LOGI("IPv4 configuration disabled");
+        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+        break;
+    }
+
+    return ret;
+}
+
+void
+nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(modem);
+
+    /* If the modem has an ethernet-type data interface (ie, not PPP and thus
+     * not point-to-point) and IP config has a /32 prefix, then we assume that
+     * ARP will be pointless and we turn it off.
+     */
+    if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
+        || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO) {
+        const NMPlatformIP4Address *address = nm_ip4_config_get_first_address(config);
+
+        g_assert(address);
+        if (address->plen == 32)
+            nm_platform_link_set_noarp(nm_device_get_platform(device),
+                                       nm_device_get_ip_ifindex(device));
+    }
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error)
+{
+    NMModemPrivate *            priv = NM_MODEM_GET_PRIVATE(self);
+    NMDedupMultiIter            ipconf_iter;
+    const NMPlatformIP6Address *addr;
+    gboolean                    do_slaac = TRUE;
+
+    if (error) {
+        g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, NULL, FALSE, error);
+        return;
+    }
+
+    if (config) {
+        /* If the IPv6 configuration only included a Link-Local address, then
+         * we have to run SLAAC to get the full IPv6 configuration.
+         */
+        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, config, &addr) {
+            if (IN6_IS_ADDR_LINKLOCAL(&addr->address)) {
+                if (!priv->iid.id)
+                    priv->iid.id = ((guint64 *) (&addr->address.s6_addr))[1];
+            } else
+                do_slaac = FALSE;
+        }
+    }
+    g_assert(config || do_slaac);
+
+    g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, config, do_slaac, NULL);
+}
+
+static NMActStageReturn
+stage3_ip6_config_request(NMModem *self, NMDeviceStateReason *out_failure_reason)
+{
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+NMActStageReturn
+nm_modem_stage3_ip6_config_start(NMModem *            self,
+                                 NMDevice *           device,
+                                 NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate * priv;
+    NMActRequest *   req;
+    NMActStageReturn ret;
+    NMConnection *   connection;
+    const char *     method;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
+
+    req = nm_device_get_act_request(device);
+    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    nm_modem_set_route_parameters_from_device(self, device);
+
+    method = nm_utils_get_ip_config_method(connection, AF_INET6);
+
+    /* Only Ignore, Disabled and Auto methods make sense for WWAN */
+    if (NM_IN_STRSET(method,
+                     NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
+                     NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
+        return NM_ACT_STAGE_RETURN_IP_DONE;
+
+    if (!nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+        _LOGW("unhandled WWAN IPv6 method '%s'; will fail", method);
+        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
+        return NM_ACT_STAGE_RETURN_FAILURE;
+    }
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    switch (priv->ip6_method) {
+    case NM_MODEM_IP_METHOD_PPP:
+        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
+        break;
+    case NM_MODEM_IP_METHOD_STATIC:
+    case NM_MODEM_IP_METHOD_AUTO:
+        /* Both static and DHCP/Auto retrieve a base IP config from the modem
+         * which in the static case is the full config, and the DHCP/Auto case
+         * is just the IPv6LL address to use for SLAAC.
+         */
+        ret = NM_MODEM_GET_CLASS(self)->stage3_ip6_config_request(self, out_failure_reason);
+        break;
+    default:
+        _LOGI("IPv6 configuration disabled");
+        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
+        break;
+    }
+
+    return ret;
+}
+
+guint32
+nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force)
+{
+    NMConnection *connection;
+    NMSetting *   setting;
+    gint64        mtu_default;
+    guint         mtu = 0;
+    const char *  property_name;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(out_source);
+
+    connection = nm_device_get_applied_connection(self);
+    if (!connection)
+        g_return_val_if_reached(0);
+
+    setting = (NMSetting *) nm_connection_get_setting_gsm(connection);
+    if (!setting)
+        setting = (NMSetting *) nm_connection_get_setting_cdma(connection);
+
+    if (setting) {
+        g_object_get(setting, "mtu", &mtu, NULL);
+        if (mtu) {
+            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+            return mtu;
+        }
+
+        property_name = NM_IS_SETTING_GSM(setting) ? "gsm.mtu" : "cdma.mtu";
+        mtu_default =
+            nm_device_get_configured_mtu_from_connection_default(self, property_name, G_MAXUINT32);
+        if (mtu_default >= 0) {
+            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
+            return (guint32) mtu_default;
+        }
+    }
+
+    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
+    return 0;
+}
+
+/*****************************************************************************/
+
+static void
+cancel_get_secrets(NMModem *self)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (priv->secrets_id)
+        nm_act_request_cancel_secrets(priv->act_request, priv->secrets_id);
+}
+
+static void
+modem_secrets_cb(NMActRequest *                req,
+                 NMActRequestGetSecretsCallId *call_id,
+                 NMSettingsConnection *        connection,
+                 GError *                      error,
+                 gpointer                      user_data)
+{
+    NMModem *       self = NM_MODEM(user_data);
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_if_fail(call_id == priv->secrets_id);
+
+    priv->secrets_id = NULL;
+
+    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)
+        || g_error_matches(error, NM_AGENT_MANAGER_ERROR, NM_AGENT_MANAGER_ERROR_NO_SECRETS))
+        return;
+
+    if (error)
+        _LOGW("modem-secrets: %s", error->message);
+
+    g_signal_emit(self, signals[AUTH_RESULT], 0, error);
+}
+
+void
+nm_modem_get_secrets(NMModem *   self,
+                     const char *setting_name,
+                     gboolean    request_new,
+                     const char *hint)
+{
+    NMModemPrivate *             priv  = NM_MODEM_GET_PRIVATE(self);
+    NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+
+    cancel_get_secrets(self);
+
+    if (request_new)
+        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+    priv->secrets_id = nm_act_request_get_secrets(priv->act_request,
+                                                  FALSE,
+                                                  setting_name,
+                                                  flags,
+                                                  NM_MAKE_STRV(hint),
+                                                  modem_secrets_cb,
+                                                  self);
+    g_return_if_fail(priv->secrets_id);
+    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+}
+
+/*****************************************************************************/
+
+static NMActStageReturn
+modem_act_stage1_prepare(NMModem *            modem,
+                         NMConnection *       connection,
+                         NMDeviceStateReason *out_failure_reason)
+{
+    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_UNKNOWN);
+    return NM_ACT_STAGE_RETURN_FAILURE;
+}
+
+NMActStageReturn
+nm_modem_act_stage1_prepare(NMModem *            self,
+                            NMActRequest *       req,
+                            NMDeviceStateReason *out_failure_reason)
+{
+    NMModemPrivate *  priv                    = NM_MODEM_GET_PRIVATE(self);
+    gs_unref_ptrarray GPtrArray *hints        = NULL;
+    const char *                 setting_name = NULL;
+    NMSecretAgentGetSecretsFlags flags        = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
+    NMConnection *               connection;
+
+    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);
+
+    if (priv->act_request)
+        g_object_unref(priv->act_request);
+    priv->act_request = g_object_ref(req);
+
+    connection = nm_act_request_get_applied_connection(req);
+    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);
+
+    setting_name = nm_connection_need_secrets(connection, &hints);
+    if (!setting_name) {
+        nm_assert(!hints);
+        return NM_MODEM_GET_CLASS(self)->modem_act_stage1_prepare(self,
+                                                                  connection,
+                                                                  out_failure_reason);
+    }
+
+    /* Secrets required... */
+    if (priv->secrets_tries++)
+        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
+
+    if (hints)
+        g_ptr_array_add(hints, NULL);
+
+    priv->secrets_id = nm_act_request_get_secrets(req,
+                                                  FALSE,
+                                                  setting_name,
+                                                  flags,
+                                                  hints ? (const char *const *) hints->pdata : NULL,
+                                                  modem_secrets_cb,
+                                                  self);
+    g_return_val_if_fail(priv->secrets_id, NM_ACT_STAGE_RETURN_FAILURE);
+    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
+    return NM_ACT_STAGE_RETURN_POSTPONE;
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_act_stage2_config(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    /* Clear secrets tries counter since secrets were successfully used
+     * already if we get here.
+     */
+    priv->secrets_tries = 0;
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_GSM_SETTING_NAME)) {
+        NMSettingGsm *s_gsm;
+        const char *  str;
+
+        s_gsm = _nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error);
+        if (!s_gsm)
+            return FALSE;
+
+        str = nm_setting_gsm_get_device_id(s_gsm);
+        if (str) {
+            if (!priv->device_id) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "GSM profile has device-id, device does not");
+                return FALSE;
+            }
+            if (!nm_streq(str, priv->device_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing device-id than GSM profile");
+                return FALSE;
+            }
+        }
+
+        /* SIM properties may not be available before the SIM is unlocked, so
+         * to ensure that autoconnect works, the connection's SIM properties
+         * are only compared if present on the device.
+         */
+
+        if (priv->sim_id && (str = nm_setting_gsm_get_sim_id(s_gsm))) {
+            if (!nm_streq(str, priv->sim_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing sim-id than GSM profile");
+                return FALSE;
+            }
+        }
+
+        if (priv->sim_operator_id && (str = nm_setting_gsm_get_sim_operator_id(s_gsm))) {
+            if (!nm_streq(str, priv->sim_operator_id)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing sim-operator-id than GSM profile");
+                return FALSE;
+            }
+        }
+    }
+
+    return NM_MODEM_GET_CLASS(self)->check_connection_compatible_with_modem(self,
+                                                                            connection,
+                                                                            error);
+}
+
+/*****************************************************************************/
+
+gboolean
+nm_modem_complete_connection(NMModem *            self,
+                             const char *         iface,
+                             NMConnection *       connection,
+                             NMConnection *const *existing_connections,
+                             GError **            error)
+{
+    NMModemClass *klass;
+
+    klass = NM_MODEM_GET_CLASS(self);
+    if (!klass->complete_connection) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_INVALID_CONNECTION,
+                    "Modem class %s had no complete_connection method",
+                    G_OBJECT_TYPE_NAME(self));
+        return FALSE;
+    }
+
+    return klass->complete_connection(self, iface, connection, existing_connections, error);
+}
+
+/*****************************************************************************/
+
+static void
+deactivate_cleanup(NMModem *self, NMDevice *device, gboolean stop_ppp_manager)
+{
+    NMModemPrivate *priv;
+    int             ifindex;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    priv->secrets_tries = 0;
+
+    if (priv->act_request) {
+        cancel_get_secrets(self);
+        g_object_unref(priv->act_request);
+        priv->act_request = NULL;
+    }
+
+    priv->in_bytes = priv->out_bytes = 0;
+
+    if (priv->ppp_manager) {
+        g_signal_handlers_disconnect_by_data(priv->ppp_manager, self);
+        if (stop_ppp_manager)
+            nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
+        g_clear_object(&priv->ppp_manager);
+    }
+
+    if (device) {
+        g_return_if_fail(NM_IS_DEVICE(device));
+
+        if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
+            || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO
+            || priv->ip6_method == NM_MODEM_IP_METHOD_STATIC
+            || priv->ip6_method == NM_MODEM_IP_METHOD_AUTO) {
+            ifindex = nm_device_get_ip_ifindex(device);
+            if (ifindex > 0) {
+                NMPlatform *platform = nm_device_get_platform(device);
+
+                nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex);
+                nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex);
+                nm_platform_link_set_down(platform, ifindex);
+            }
+        }
+    }
+
+    nm_clear_g_free(&priv->data_port);
+    priv->mm_ip_timeout = 0;
+    priv->ip4_method    = NM_MODEM_IP_METHOD_UNKNOWN;
+    priv->ip6_method    = NM_MODEM_IP_METHOD_UNKNOWN;
+    _set_ip_ifindex(self, -1, NULL);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMModem *                 self;
+    NMDevice *                device;
+    GCancellable *            cancellable;
+    NMModemDeactivateCallback callback;
+    gpointer                  callback_user_data;
+} DeactivateContext;
+
+static void
+deactivate_context_complete(DeactivateContext *ctx, GError *error)
+{
+    NMModem *self = ctx->self;
+
+    _LOGD("modem deactivation finished %s%s%s",
+          NM_PRINT_FMT_QUOTED(error, "with failure: ", error->message, "", "successfully"));
+
+    if (ctx->callback)
+        ctx->callback(ctx->self, error, ctx->callback_user_data);
+    nm_g_object_unref(ctx->cancellable);
+    g_object_unref(ctx->device);
+    g_object_unref(ctx->self);
+    g_slice_free(DeactivateContext, ctx);
+}
+
+static void
+_deactivate_call_disconnect_cb(NMModem *self, GError *error, gpointer user_data)
+{
+    deactivate_context_complete(user_data, error);
+}
+
+static void
+_deactivate_call_disconnect(DeactivateContext *ctx)
+{
+    NM_MODEM_GET_CLASS(ctx->self)->disconnect(ctx->self,
+                                              FALSE,
+                                              ctx->cancellable,
+                                              _deactivate_call_disconnect_cb,
+                                              ctx);
+}
+
+static void
+_deactivate_ppp_manager_stop_cb(NMPPPManager *          ppp_manager,
+                                NMPPPManagerStopHandle *handle,
+                                gboolean                was_cancelled,
+                                gpointer                user_data)
+{
+    DeactivateContext *ctx = user_data;
+
+    g_object_unref(ppp_manager);
+
+    if (was_cancelled) {
+        gs_free_error GError *error = NULL;
+
+        if (!g_cancellable_set_error_if_cancelled(ctx->cancellable, &error))
+            nm_assert_not_reached();
+        deactivate_context_complete(ctx, error);
+        return;
+    }
+
+    nm_assert(!g_cancellable_is_cancelled(ctx->cancellable));
+    _deactivate_call_disconnect(ctx);
+}
+
+void
+nm_modem_deactivate_async(NMModem *                 self,
+                          NMDevice *                device,
+                          GCancellable *            cancellable,
+                          NMModemDeactivateCallback callback,
+                          gpointer                  user_data)
+{
+    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
+    DeactivateContext *ctx;
+    NMPPPManager *     ppp_manager;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+    g_return_if_fail(NM_IS_DEVICE(device));
+    g_return_if_fail(G_IS_CANCELLABLE(cancellable));
+
+    ctx                     = g_slice_new(DeactivateContext);
+    ctx->self               = g_object_ref(self);
+    ctx->device             = g_object_ref(device);
+    ctx->cancellable        = g_object_ref(cancellable);
+    ctx->callback           = callback;
+    ctx->callback_user_data = user_data;
+
+    ppp_manager = nm_g_object_ref(priv->ppp_manager);
+
+    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, ctx->device, FALSE);
+
+    if (ppp_manager) {
+        /* If we have a PPP manager, stop it.
+         *
+         * Pass on the reference in @ppp_manager. */
+        nm_ppp_manager_stop(ppp_manager, ctx->cancellable, _deactivate_ppp_manager_stop_cb, ctx);
+        return;
+    }
+
+    _deactivate_call_disconnect(ctx);
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_deactivate(NMModem *self, NMDevice *device)
+{
+    /* First cleanup */
+    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, device, TRUE);
+    /* Then disconnect without waiting */
+    NM_MODEM_GET_CLASS(self)->disconnect(self, FALSE, NULL, NULL, NULL);
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_device_state_changed(NMModem *self, NMDeviceState new_state, NMDeviceState old_state)
+{
+    gboolean        was_connected = FALSE, warn = TRUE;
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    if (old_state >= NM_DEVICE_STATE_PREPARE && old_state <= NM_DEVICE_STATE_DEACTIVATING)
+        was_connected = TRUE;
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* Make sure we don't leave the serial device open */
+    switch (new_state) {
+    case NM_DEVICE_STATE_UNMANAGED:
+    case NM_DEVICE_STATE_UNAVAILABLE:
+    case NM_DEVICE_STATE_FAILED:
+    case NM_DEVICE_STATE_DISCONNECTED:
+        if (priv->act_request) {
+            cancel_get_secrets(self);
+            g_object_unref(priv->act_request);
+            priv->act_request = NULL;
+        }
+
+        if (was_connected) {
+            /* Don't bother warning on FAILED since the modem is already gone */
+            if (new_state == NM_DEVICE_STATE_FAILED || new_state == NM_DEVICE_STATE_DISCONNECTED)
+                warn = FALSE;
+            /* First cleanup */
+            NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, NULL, TRUE);
+            NM_MODEM_GET_CLASS(self)->disconnect(self, warn, NULL, NULL, NULL);
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+const char *
+nm_modem_get_uid(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->uid;
+}
+
+const char *
+nm_modem_get_path(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->path;
+}
+
+const char *
+nm_modem_get_driver(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->driver;
+}
+
+const char *
+nm_modem_get_control_port(NMModem *self)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), NULL);
+
+    return NM_MODEM_GET_PRIVATE(self)->control_port;
+}
+
+int
+nm_modem_get_ip_ifindex(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), 0);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    /* internally we track an unset ip_ifindex as -1.
+     * For the caller of nm_modem_get_ip_ifindex(), this
+     * shall be zero too. */
+    return priv->ip_ifindex != -1 ? priv->ip_ifindex : 0;
+}
+
+static void
+_set_ip_ifindex(NMModem *self, int ifindex, const char *ifname)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    nm_assert(ifindex >= -1);
+    nm_assert((ifindex > 0) == !!ifname);
+
+    if (!nm_streq0(priv->ip_iface, ifname)) {
+        g_free(priv->ip_iface);
+        priv->ip_iface = g_strdup(ifname);
+    }
+
+    if (priv->ip_ifindex != ifindex) {
+        priv->ip_ifindex = ifindex;
+        _notify(self, PROP_IP_IFINDEX);
+    }
+}
+
+gboolean
+nm_modem_set_data_port(NMModem *       self,
+                       NMPlatform *    platform,
+                       const char *    data_port,
+                       NMModemIPMethod ip4_method,
+                       NMModemIPMethod ip6_method,
+                       guint           timeout,
+                       GError **       error)
+{
+    NMModemPrivate *priv;
+    gboolean        is_ppp;
+    int             ifindex = -1;
+
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+    g_return_val_if_fail(NM_IS_PLATFORM(platform), FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (priv->ppp_manager || priv->data_port || priv->ip_ifindex != -1) {
+        g_set_error_literal(error,
+                            NM_UTILS_ERROR,
+                            NM_UTILS_ERROR_UNKNOWN,
+                            "cannot set data port in activated state");
+        /* this really shouldn't happen. Assert. */
+        g_return_val_if_reached(FALSE);
+    }
+
+    if (!data_port) {
+        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "missing data port");
+        return FALSE;
+    }
+
+    is_ppp = (ip4_method == NM_MODEM_IP_METHOD_PPP) || (ip6_method == NM_MODEM_IP_METHOD_PPP);
+    if (is_ppp) {
+        if (!NM_IN_SET(ip4_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)
+            || !NM_IN_SET(ip6_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)) {
+            g_set_error_literal(error,
+                                NM_UTILS_ERROR,
+                                NM_UTILS_ERROR_UNKNOWN,
+                                "conflicting ip methods");
+            return FALSE;
+        }
+    } else if (!NM_IN_SET(ip4_method,
+                          NM_MODEM_IP_METHOD_UNKNOWN,
+                          NM_MODEM_IP_METHOD_STATIC,
+                          NM_MODEM_IP_METHOD_AUTO)
+               || !NM_IN_SET(ip6_method,
+                             NM_MODEM_IP_METHOD_UNKNOWN,
+                             NM_MODEM_IP_METHOD_STATIC,
+                             NM_MODEM_IP_METHOD_AUTO)
+               || (ip4_method == NM_MODEM_IP_METHOD_UNKNOWN
+                   && ip6_method == NM_MODEM_IP_METHOD_UNKNOWN)) {
+        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "invalid ip methods");
+        return FALSE;
+    }
+
+    if (!is_ppp) {
+        ifindex = nm_platform_if_nametoindex(platform, data_port);
+        if (ifindex <= 0) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        "cannot find network interface %s",
+                        data_port);
+            return FALSE;
+        }
+        if (!nm_platform_process_events_ensure_link(platform, ifindex, data_port)) {
+            g_set_error(error,
+                        NM_UTILS_ERROR,
+                        NM_UTILS_ERROR_UNKNOWN,
+                        "cannot find network interface %s in platform cache",
+                        data_port);
+            return FALSE;
+        }
+    }
+
+    priv->mm_ip_timeout = timeout;
+    priv->ip4_method    = ip4_method;
+    priv->ip6_method    = ip6_method;
+    if (is_ppp) {
+        priv->data_port = g_strdup(data_port);
+        _set_ip_ifindex(self, -1, NULL);
+    } else {
+        priv->data_port = NULL;
+        _set_ip_ifindex(self, ifindex, data_port);
+    }
+    return TRUE;
+}
+
+gboolean
+nm_modem_owns_port(NMModem *self, const char *iface)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    g_return_val_if_fail(iface != NULL, FALSE);
+
+    if (NM_MODEM_GET_CLASS(self)->owns_port)
+        return NM_MODEM_GET_CLASS(self)->owns_port(self, iface);
+
+    return NM_IN_STRSET(iface, priv->ip_iface, priv->data_port, priv->control_port);
+}
+
+gboolean
+nm_modem_get_iid(NMModem *self, NMUtilsIPv6IfaceId *out_iid)
+{
+    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
+
+    *out_iid = NM_MODEM_GET_PRIVATE(self)->iid;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_get_route_parameters(NMModem *self,
+                              guint32 *out_ip4_route_table,
+                              guint32 *out_ip4_route_metric,
+                              guint32 *out_ip6_route_table,
+                              guint32 *out_ip6_route_metric)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    NM_SET_OUT(out_ip4_route_table, priv->ip4_route_table);
+    NM_SET_OUT(out_ip4_route_metric, priv->ip4_route_metric);
+    NM_SET_OUT(out_ip6_route_table, priv->ip6_route_table);
+    NM_SET_OUT(out_ip6_route_metric, priv->ip6_route_metric);
+}
+
+void
+nm_modem_set_route_parameters(NMModem *self,
+                              guint32  ip4_route_table,
+                              guint32  ip4_route_metric,
+                              guint32  ip6_route_table,
+                              guint32  ip6_route_metric)
+{
+    NMModemPrivate *priv;
+
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    priv = NM_MODEM_GET_PRIVATE(self);
+    if (priv->ip4_route_table != ip4_route_table || priv->ip4_route_metric != ip4_route_metric
+        || priv->ip6_route_table != ip6_route_table || priv->ip6_route_metric != ip6_route_metric) {
+        priv->ip4_route_table  = ip4_route_table;
+        priv->ip4_route_metric = ip4_route_metric;
+        priv->ip6_route_table  = ip6_route_table;
+        priv->ip6_route_metric = ip6_route_metric;
+
+        _LOGT("route-parameters: table-v4: %u, metric-v4: %u, table-v6: %u, metric-v6: %u",
+              priv->ip4_route_table,
+              priv->ip4_route_metric,
+              priv->ip6_route_table,
+              priv->ip6_route_metric);
+    }
+
+    if (priv->ppp_manager) {
+        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
+                                            priv->ip4_route_table,
+                                            priv->ip4_route_metric,
+                                            priv->ip6_route_table,
+                                            priv->ip6_route_metric);
+    }
+}
+
+void
+nm_modem_set_route_parameters_from_device(NMModem *self, NMDevice *device)
+{
+    g_return_if_fail(NM_IS_DEVICE(device));
+
+    nm_modem_set_route_parameters(self,
+                                  nm_device_get_route_table(device, AF_INET),
+                                  nm_device_get_route_metric(device, AF_INET),
+                                  nm_device_get_route_table(device, AF_INET6),
+                                  nm_device_get_route_metric(device, AF_INET6));
+}
+
+/*****************************************************************************/
+
+void
+nm_modem_get_capabilities(NMModem *                  self,
+                          NMDeviceModemCapabilities *modem_caps,
+                          NMDeviceModemCapabilities *current_caps)
+{
+    g_return_if_fail(NM_IS_MODEM(self));
+
+    NM_MODEM_GET_CLASS(self)->get_capabilities(self, modem_caps, current_caps);
+}
+
+/*****************************************************************************/
+
+void
+_nm_modem_set_operator_code(NMModem *self, const char *operator_code)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (g_strcmp0(priv->operator_code, operator_code) != 0) {
+        g_free(priv->operator_code);
+        priv->operator_code = g_strdup(operator_code);
+        _notify(self, PROP_OPERATOR_CODE);
+    }
+}
+
+void
+_nm_modem_set_apn(NMModem *self, const char *apn)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    if (g_strcmp0(priv->apn, apn) != 0) {
+        g_free(priv->apn);
+        priv->apn = g_strdup(apn);
+        _notify(self, PROP_APN);
+    }
+}
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMModem *       self = NM_MODEM(object);
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);
+
+    switch (prop_id) {
+    case PROP_PATH:
+        g_value_set_string(value, priv->path);
+        break;
+    case PROP_DRIVER:
+        g_value_set_string(value, priv->driver);
+        break;
+    case PROP_CONTROL_PORT:
+        g_value_set_string(value, priv->control_port);
+        break;
+    case PROP_IP_IFINDEX:
+        g_value_set_int(value, nm_modem_get_ip_ifindex(self));
+        break;
+    case PROP_UID:
+        g_value_set_string(value, priv->uid);
+        break;
+    case PROP_STATE:
+        g_value_set_int(value, priv->state);
+        break;
+    case PROP_DEVICE_ID:
+        g_value_set_string(value, priv->device_id);
+        break;
+    case PROP_SIM_ID:
+        g_value_set_string(value, priv->sim_id);
+        break;
+    case PROP_IP_TYPES:
+        g_value_set_uint(value, priv->ip_types);
+        break;
+    case PROP_SIM_OPERATOR_ID:
+        g_value_set_string(value, priv->sim_operator_id);
+        break;
+    case PROP_OPERATOR_CODE:
+        g_value_set_string(value, priv->operator_code);
+        break;
+    case PROP_APN:
+        g_value_set_string(value, priv->apn);
+        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)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+    const char *    s;
+
+    switch (prop_id) {
+    case PROP_PATH:
+        /* construct-only */
+        priv->path = g_value_dup_string(value);
+        g_return_if_fail(priv->path);
+        break;
+    case PROP_DRIVER:
+        /* construct-only */
+        priv->driver = g_value_dup_string(value);
+        break;
+    case PROP_CONTROL_PORT:
+        /* construct-only */
+        priv->control_port = g_value_dup_string(value);
+        break;
+    case PROP_UID:
+        /* construct-only */
+        priv->uid = g_value_dup_string(value);
+        break;
+    case PROP_STATE:
+        /* construct-only */
+        priv->state = g_value_get_int(value);
+        break;
+    case PROP_DEVICE_ID:
+        /* construct-only */
+        priv->device_id = g_value_dup_string(value);
+        break;
+    case PROP_SIM_ID:
+        g_free(priv->sim_id);
+        priv->sim_id = g_value_dup_string(value);
+        break;
+    case PROP_IP_TYPES:
+        priv->ip_types = g_value_get_uint(value);
+        break;
+    case PROP_SIM_OPERATOR_ID:
+        nm_clear_g_free(&priv->sim_operator_id);
+        s = g_value_get_string(value);
+        if (s && s[0])
+            priv->sim_operator_id = g_strdup(s);
+        break;
+    case PROP_OPERATOR_CODE:
+        /* construct-only */
+        priv->operator_code = g_value_dup_string(value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+/*****************************************************************************/
+
+static void
+nm_modem_init(NMModem *self)
+{
+    NMModemPrivate *priv;
+
+    self->_priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_MODEM, NMModemPrivate);
+    priv        = self->_priv;
+
+    priv->ip_ifindex       = -1;
+    priv->ip4_route_table  = RT_TABLE_MAIN;
+    priv->ip4_route_metric = 700;
+    priv->ip6_route_table  = RT_TABLE_MAIN;
+    priv->ip6_route_metric = 700;
+}
+
+static void
+constructed(GObject *object)
+{
+    NMModemPrivate *priv;
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->constructed(object);
+
+    priv = NM_MODEM_GET_PRIVATE(NM_MODEM(object));
+
+    g_return_if_fail(priv->control_port);
+}
+
+/*****************************************************************************/
+
+static void
+dispose(GObject *object)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+
+    g_clear_object(&priv->act_request);
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->dispose(object);
+}
+
+static void
+finalize(GObject *object)
+{
+    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
+
+    g_free(priv->uid);
+    g_free(priv->path);
+    g_free(priv->driver);
+    g_free(priv->control_port);
+    g_free(priv->data_port);
+    g_free(priv->ip_iface);
+    g_free(priv->device_id);
+    g_free(priv->sim_id);
+    g_free(priv->sim_operator_id);
+    g_free(priv->operator_code);
+    g_free(priv->apn);
+
+    G_OBJECT_CLASS(nm_modem_parent_class)->finalize(object);
+}
+
+static void
+nm_modem_class_init(NMModemClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+    g_type_class_add_private(object_class, sizeof(NMModemPrivate));
+
+    object_class->constructed  = constructed;
+    object_class->set_property = set_property;
+    object_class->get_property = get_property;
+    object_class->dispose      = dispose;
+    object_class->finalize     = finalize;
+
+    klass->modem_act_stage1_prepare  = modem_act_stage1_prepare;
+    klass->stage3_ip6_config_request = stage3_ip6_config_request;
+    klass->deactivate_cleanup        = deactivate_cleanup;
+
+    obj_properties[PROP_UID] =
+        g_param_spec_string(NM_MODEM_UID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PATH] =
+        g_param_spec_string(NM_MODEM_PATH,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DRIVER] =
+        g_param_spec_string(NM_MODEM_DRIVER,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_CONTROL_PORT] =
+        g_param_spec_string(NM_MODEM_CONTROL_PORT,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_IP_IFINDEX] = g_param_spec_int(NM_MODEM_IP_IFINDEX,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       G_MAXINT,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_STATE] =
+        g_param_spec_int(NM_MODEM_STATE,
+                         "",
+                         "",
+                         NM_MODEM_STATE_UNKNOWN,
+                         _NM_MODEM_STATE_LAST,
+                         NM_MODEM_STATE_UNKNOWN,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_DEVICE_ID] =
+        g_param_spec_string(NM_MODEM_DEVICE_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SIM_ID] =
+        g_param_spec_string(NM_MODEM_SIM_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_IP_TYPES] =
+        g_param_spec_uint(NM_MODEM_IP_TYPES,
+                          "IP Types",
+                          "Supported IP types",
+                          0,
+                          G_MAXUINT32,
+                          NM_MODEM_IP_TYPE_IPV4,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SIM_OPERATOR_ID] =
+        g_param_spec_string(NM_MODEM_SIM_OPERATOR_ID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_OPERATOR_CODE] =
+        g_param_spec_string(NM_MODEM_OPERATOR_CODE,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_APN] =
+        g_param_spec_string(NM_MODEM_APN, "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+
+    signals[PPP_STATS] = g_signal_new(NM_MODEM_PPP_STATS,
+                                      G_OBJECT_CLASS_TYPE(object_class),
+                                      G_SIGNAL_RUN_FIRST,
+                                      0,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      G_TYPE_NONE,
+                                      2,
+                                      G_TYPE_UINT /*guint32 in_bytes*/,
+                                      G_TYPE_UINT /*guint32 out_bytes*/);
+
+    signals[PPP_FAILED] = g_signal_new(NM_MODEM_PPP_FAILED,
+                                       G_OBJECT_CLASS_TYPE(object_class),
+                                       G_SIGNAL_RUN_FIRST,
+                                       0,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       G_TYPE_NONE,
+                                       1,
+                                       G_TYPE_UINT);
+
+    signals[IP4_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP4_CONFIG_RESULT,
+                                              G_OBJECT_CLASS_TYPE(object_class),
+                                              G_SIGNAL_RUN_FIRST,
+                                              0,
+                                              NULL,
+                                              NULL,
+                                              NULL,
+                                              G_TYPE_NONE,
+                                              2,
+                                              G_TYPE_OBJECT,
+                                              G_TYPE_POINTER);
+
+    /**
+     * NMModem::ip6-config-result:
+     * @modem: the #NMModem  on which the signal is emitted
+     * @config: the #NMIP6Config to apply to the modem's data port
+     * @do_slaac: %TRUE if IPv6 SLAAC should be started
+     * @error: a #GError if any error occurred during IP configuration
+     *
+     * This signal is emitted when IPv6 configuration has completed or failed.
+     * If @error is set the configuration failed.  If @config is set, then
+     * the details should be applied to the data port before any further
+     * configuration (like SLAAC) is done.  @do_slaac indicates whether SLAAC
+     * should be started after applying @config to the data port.
+     */
+    signals[IP6_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP6_CONFIG_RESULT,
+                                              G_OBJECT_CLASS_TYPE(object_class),
+                                              G_SIGNAL_RUN_FIRST,
+                                              0,
+                                              NULL,
+                                              NULL,
+                                              NULL,
+                                              G_TYPE_NONE,
+                                              3,
+                                              G_TYPE_OBJECT,
+                                              G_TYPE_BOOLEAN,
+                                              G_TYPE_POINTER);
+
+    signals[PREPARE_RESULT] = g_signal_new(NM_MODEM_PREPARE_RESULT,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_FIRST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           2,
+                                           G_TYPE_BOOLEAN,
+                                           G_TYPE_UINT);
+
+    signals[AUTH_REQUESTED] = g_signal_new(NM_MODEM_AUTH_REQUESTED,
+                                           G_OBJECT_CLASS_TYPE(object_class),
+                                           G_SIGNAL_RUN_FIRST,
+                                           0,
+                                           NULL,
+                                           NULL,
+                                           NULL,
+                                           G_TYPE_NONE,
+                                           0);
+
+    signals[AUTH_RESULT] = g_signal_new(NM_MODEM_AUTH_RESULT,
+                                        G_OBJECT_CLASS_TYPE(object_class),
+                                        G_SIGNAL_RUN_FIRST,
+                                        0,
+                                        NULL,
+                                        NULL,
+                                        NULL,
+                                        G_TYPE_NONE,
+                                        1,
+                                        G_TYPE_POINTER);
+
+    signals[REMOVED] = g_signal_new(NM_MODEM_REMOVED,
+                                    G_OBJECT_CLASS_TYPE(object_class),
+                                    G_SIGNAL_RUN_FIRST,
+                                    0,
+                                    NULL,
+                                    NULL,
+                                    NULL,
+                                    G_TYPE_NONE,
+                                    0);
+
+    signals[STATE_CHANGED] = g_signal_new(NM_MODEM_STATE_CHANGED,
+                                          G_OBJECT_CLASS_TYPE(object_class),
+                                          G_SIGNAL_RUN_FIRST,
+                                          0,
+                                          NULL,
+                                          NULL,
+                                          NULL,
+                                          G_TYPE_NONE,
+                                          2,
+                                          G_TYPE_INT,
+                                          G_TYPE_INT);
+}
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
new file mode 100644
index 00000000..87162cfc
--- /dev/null
+++ b/src/core/devices/wwan/nm-modem.h
@@ -0,0 +1,269 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2009 - 2011 Red Hat, Inc.
+ * Copyright (C) 2009 Novell, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_MODEM_H__
+#define __NETWORKMANAGER_MODEM_H__
+
+#include "ppp/nm-ppp-manager.h"
+#include "devices/nm-device.h"
+
+#define NM_TYPE_MODEM            (nm_modem_get_type())
+#define NM_MODEM(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MODEM, NMModem))
+#define NM_MODEM_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_MODEM, NMModemClass))
+#define NM_IS_MODEM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_MODEM))
+#define NM_IS_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_MODEM))
+#define NM_MODEM_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_MODEM, NMModemClass))
+
+/* Properties */
+#define NM_MODEM_UID             "uid"
+#define NM_MODEM_PATH            "path"
+#define NM_MODEM_DRIVER          "driver"
+#define NM_MODEM_CONTROL_PORT    "control-port"
+#define NM_MODEM_IP_IFINDEX      "ip-ifindex"
+#define NM_MODEM_STATE           "state"
+#define NM_MODEM_DEVICE_ID       "device-id"
+#define NM_MODEM_SIM_ID          "sim-id"
+#define NM_MODEM_IP_TYPES        "ip-types" /* Supported IP types */
+#define NM_MODEM_SIM_OPERATOR_ID "sim-operator-id"
+#define NM_MODEM_OPERATOR_CODE   "operator-code"
+#define NM_MODEM_APN             "apn"
+
+/* Signals */
+#define NM_MODEM_PPP_STATS         "ppp-stats"
+#define NM_MODEM_PPP_FAILED        "ppp-failed"
+#define NM_MODEM_PREPARE_RESULT    "prepare-result"
+#define NM_MODEM_IP4_CONFIG_RESULT "ip4-config-result"
+#define NM_MODEM_IP6_CONFIG_RESULT "ip6-config-result"
+#define NM_MODEM_AUTH_REQUESTED    "auth-requested"
+#define NM_MODEM_AUTH_RESULT       "auth-result"
+#define NM_MODEM_REMOVED           "removed"
+#define NM_MODEM_STATE_CHANGED     "state-changed"
+
+typedef enum {
+    NM_MODEM_IP_METHOD_UNKNOWN = 0,
+    NM_MODEM_IP_METHOD_PPP,
+    NM_MODEM_IP_METHOD_STATIC,
+    NM_MODEM_IP_METHOD_AUTO, /* DHCP and/or SLAAC */
+} NMModemIPMethod;
+
+/**
+ * NMModemIPType:
+ * @NM_MODEM_IP_TYPE_UNKNOWN: unknown or no IP support
+ * @NM_MODEM_IP_TYPE_IPV4: IPv4-only bearers are supported
+ * @NM_MODEM_IP_TYPE_IPV6: IPv6-only bearers are supported
+ * @NM_MODEM_IP_TYPE_IPV4V6: dual-stack IPv4 + IPv6 bearers are supported
+ *
+ * Indicates what IP protocols the modem supports for an IP bearer.  Any
+ * combination of flags is possible.  For example, (%NM_MODEM_IP_TYPE_IPV4 |
+ * %NM_MODEM_IP_TYPE_IPV6) indicates that the modem supports IPv4 and IPv6
+ * but not simultaneously on the same bearer.
+ */
+typedef enum {
+    NM_MODEM_IP_TYPE_UNKNOWN = 0x0,
+    NM_MODEM_IP_TYPE_IPV4    = 0x1,
+    NM_MODEM_IP_TYPE_IPV6    = 0x2,
+    NM_MODEM_IP_TYPE_IPV4V6  = 0x4
+} NMModemIPType;
+
+typedef enum { /*< underscore_name=nm_modem_state >*/
+               NM_MODEM_STATE_UNKNOWN       = 0,
+               NM_MODEM_STATE_FAILED        = 1,
+               NM_MODEM_STATE_INITIALIZING  = 2,
+               NM_MODEM_STATE_LOCKED        = 3,
+               NM_MODEM_STATE_DISABLED      = 4,
+               NM_MODEM_STATE_DISABLING     = 5,
+               NM_MODEM_STATE_ENABLING      = 6,
+               NM_MODEM_STATE_ENABLED       = 7,
+               NM_MODEM_STATE_SEARCHING     = 8,
+               NM_MODEM_STATE_REGISTERED    = 9,
+               NM_MODEM_STATE_DISCONNECTING = 10,
+               NM_MODEM_STATE_CONNECTING    = 11,
+               NM_MODEM_STATE_CONNECTED     = 12,
+
+               _NM_MODEM_STATE_LAST0,
+               _NM_MODEM_STATE_LAST = _NM_MODEM_STATE_LAST0 - 1,
+} NMModemState;
+
+struct _NMModemPrivate;
+
+struct _NMModem {
+    GObject                 parent;
+    struct _NMModemPrivate *_priv;
+};
+
+typedef struct _NMModem NMModem;
+
+typedef void (*_NMModemDisconnectCallback)(NMModem *modem, GError *error, gpointer user_data);
+
+typedef struct {
+    GObjectClass parent;
+
+    void (*get_capabilities)(NMModem *                  self,
+                             NMDeviceModemCapabilities *modem_caps,
+                             NMDeviceModemCapabilities *current_caps);
+
+    gboolean (*get_user_pass)(NMModem *     modem,
+                              NMConnection *connection,
+                              const char ** user,
+                              const char ** pass);
+
+    gboolean (*check_connection_compatible_with_modem)(NMModem *     modem,
+                                                       NMConnection *connection,
+                                                       GError **     error);
+
+    gboolean (*complete_connection)(NMModem *            modem,
+                                    const char *         iface,
+                                    NMConnection *       connection,
+                                    NMConnection *const *existing_connections,
+                                    GError **            error);
+
+    NMActStageReturn (*modem_act_stage1_prepare)(NMModem *            modem,
+                                                 NMConnection *       connection,
+                                                 NMDeviceStateReason *out_failure_reason);
+
+    NMActStageReturn (*static_stage3_ip4_config_start)(NMModem *            self,
+                                                       NMActRequest *       req,
+                                                       NMDeviceStateReason *out_failure_reason);
+
+    /* Request the IP6 config; when the config returns the modem
+     * subclass should emit the ip6_config_result signal.
+     */
+    NMActStageReturn (*stage3_ip6_config_request)(NMModem *            self,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+    void (*set_mm_enabled)(NMModem *self, gboolean enabled);
+
+    void (*disconnect)(NMModem *                  self,
+                       gboolean                   warn,
+                       GCancellable *             cancellable,
+                       _NMModemDisconnectCallback callback,
+                       gpointer                   user_data);
+
+    void (*deactivate_cleanup)(NMModem *self, NMDevice *device, gboolean stop_ppp_manager);
+
+    gboolean (*owns_port)(NMModem *self, const char *iface);
+} NMModemClass;
+
+GType nm_modem_get_type(void);
+
+gboolean nm_modem_is_claimed(NMModem *modem);
+NMModem *nm_modem_claim(NMModem *modem);
+void     nm_modem_unclaim(NMModem *modem);
+
+const char *nm_modem_get_path(NMModem *modem);
+const char *nm_modem_get_uid(NMModem *modem);
+const char *nm_modem_get_control_port(NMModem *modem);
+int         nm_modem_get_ip_ifindex(NMModem *modem);
+const char *nm_modem_get_driver(NMModem *modem);
+const char *nm_modem_get_device_id(NMModem *modem);
+const char *nm_modem_get_sim_id(NMModem *modem);
+const char *nm_modem_get_sim_operator_id(NMModem *modem);
+gboolean    nm_modem_get_iid(NMModem *modem, NMUtilsIPv6IfaceId *out_iid);
+const char *nm_modem_get_operator_code(NMModem *modem);
+const char *nm_modem_get_apn(NMModem *modem);
+
+gboolean nm_modem_set_data_port(NMModem *       self,
+                                NMPlatform *    platform,
+                                const char *    data_port,
+                                NMModemIPMethod ip4_method,
+                                NMModemIPMethod ip6_method,
+                                guint           timeout,
+                                GError **       error);
+
+gboolean nm_modem_owns_port(NMModem *modem, const char *iface);
+
+void nm_modem_get_capabilities(NMModem *                  self,
+                               NMDeviceModemCapabilities *modem_caps,
+                               NMDeviceModemCapabilities *current_caps);
+
+gboolean
+nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error);
+
+gboolean nm_modem_complete_connection(NMModem *            self,
+                                      const char *         iface,
+                                      NMConnection *       connection,
+                                      NMConnection *const *existing_connections,
+                                      GError **            error);
+
+void nm_modem_get_route_parameters(NMModem *self,
+                                   guint32 *out_ip4_route_table,
+                                   guint32 *out_ip4_route_metric,
+                                   guint32 *out_ip6_route_table,
+                                   guint32 *out_ip6_route_metric);
+
+void nm_modem_set_route_parameters(NMModem *self,
+                                   guint32  ip4_route_table,
+                                   guint32  ip4_route_metric,
+                                   guint32  ip6_route_table,
+                                   guint32  ip6_route_metric);
+
+void nm_modem_set_route_parameters_from_device(NMModem *modem, NMDevice *device);
+
+NMActStageReturn nm_modem_act_stage1_prepare(NMModem *            modem,
+                                             NMActRequest *       req,
+                                             NMDeviceStateReason *out_failure_reason);
+
+void nm_modem_act_stage2_config(NMModem *modem);
+
+NMActStageReturn nm_modem_stage3_ip4_config_start(NMModem *            modem,
+                                                  NMDevice *           device,
+                                                  NMDeviceClass *      device_class,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+NMActStageReturn nm_modem_stage3_ip6_config_start(NMModem *            modem,
+                                                  NMDevice *           device,
+                                                  NMDeviceStateReason *out_failure_reason);
+
+void nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config);
+
+void nm_modem_get_secrets(NMModem *   modem,
+                          const char *setting_name,
+                          gboolean    request_new,
+                          const char *hint);
+
+void nm_modem_deactivate(NMModem *modem, NMDevice *device);
+
+typedef void (*NMModemDeactivateCallback)(NMModem *self, GError *error, gpointer user_data);
+
+void nm_modem_deactivate_async(NMModem *                 self,
+                               NMDevice *                device,
+                               GCancellable *            cancellable,
+                               NMModemDeactivateCallback callback,
+                               gpointer                  user_data);
+
+void
+nm_modem_device_state_changed(NMModem *modem, NMDeviceState new_state, NMDeviceState old_state);
+
+void nm_modem_set_mm_enabled(NMModem *self, gboolean enabled);
+
+NMModemState nm_modem_get_state(NMModem *self);
+void         nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason);
+void         nm_modem_set_prev_state(NMModem *self, const char *reason);
+const char * nm_modem_state_to_string(NMModemState state);
+
+NMModemIPType nm_modem_get_supported_ip_types(NMModem *self);
+
+/* For the modem-manager only */
+void nm_modem_emit_removed(NMModem *self);
+
+void nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason);
+
+void nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason);
+
+GArray *nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error);
+
+/* For subclasses */
+void nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error);
+
+const char *nm_modem_ip_type_to_string(NMModemIPType ip_type);
+
+guint32
+nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force);
+
+void _nm_modem_set_operator_code(NMModem *self, const char *operator_code);
+void _nm_modem_set_apn(NMModem *self, const char *apn);
+
+#endif /* __NETWORKMANAGER_MODEM_H__ */
diff --git a/src/core/devices/wwan/nm-service-providers.c b/src/core/devices/wwan/nm-service-providers.c
new file mode 100644
index 00000000..89add6c5
--- /dev/null
+++ b/src/core/devices/wwan/nm-service-providers.c
@@ -0,0 +1,455 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2009 Novell, Inc.
+ * Author: Tambet Ingo (tambet@gmail.com).
+ * Copyright (C) 2009 - 2019 Red Hat, Inc.
+ * Copyright (C) 2012 Lanedo GmbH
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-service-providers.h"
+
+typedef enum {
+    PARSER_TOPLEVEL = 0,
+    PARSER_COUNTRY,
+    PARSER_PROVIDER,
+    PARSER_METHOD_GSM,
+    PARSER_METHOD_GSM_APN,
+    PARSER_METHOD_CDMA,
+    PARSER_DONE,
+    PARSER_ERROR
+} ParseContextState;
+
+typedef struct {
+    char *                           mccmnc;
+    NMServiceProvidersGsmApnCallback callback;
+    gpointer                         user_data;
+    GCancellable *                   cancellable;
+    GMarkupParseContext *            ctx;
+    char                             buffer[4096];
+
+    char *            text_buffer;
+    ParseContextState state;
+
+    gboolean mccmnc_matched;
+    gboolean found_internet_apn;
+    char *   apn;
+    char *   username;
+    char *   password;
+    char *   gateway;
+    char *   auth_method;
+    GSList * dns;
+} ParseContext;
+
+/*****************************************************************************/
+
+static void
+parser_toplevel_start(ParseContext *parse_context,
+                      const char *  name,
+                      const char ** attribute_names,
+                      const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "serviceproviders") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "format") == 0) {
+                if (strcmp(attribute_values[i], "2.0")) {
+                    g_warning("%s: mobile broadband provider database format '%s'"
+                              " not supported.",
+                              __func__,
+                              attribute_values[i]);
+                    parse_context->state = PARSER_ERROR;
+                    break;
+                }
+            }
+        }
+    } else if (strcmp(name, "country") == 0) {
+        parse_context->state = PARSER_COUNTRY;
+    }
+}
+
+static void
+parser_country_start(ParseContext *parse_context,
+                     const char *  name,
+                     const char ** attribute_names,
+                     const char ** attribute_values)
+{
+    if (strcmp(name, "provider") == 0)
+        parse_context->state = PARSER_PROVIDER;
+}
+
+static void
+parser_provider_start(ParseContext *parse_context,
+                      const char *  name,
+                      const char ** attribute_names,
+                      const char ** attribute_values)
+{
+    parse_context->mccmnc_matched = FALSE;
+    if (strcmp(name, "gsm") == 0)
+        parse_context->state = PARSER_METHOD_GSM;
+    else if (strcmp(name, "cdma") == 0)
+        parse_context->state = PARSER_METHOD_CDMA;
+}
+
+static void
+parser_gsm_start(ParseContext *parse_context,
+                 const char *  name,
+                 const char ** attribute_names,
+                 const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "network-id") == 0) {
+        const char *mcc = NULL, *mnc = NULL;
+
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "mcc") == 0)
+                mcc = attribute_values[i];
+            else if (strcmp(attribute_names[i], "mnc") == 0)
+                mnc = attribute_values[i];
+            if (mcc && strlen(mcc) && mnc && strlen(mnc)) {
+                char *mccmnc = g_strdup_printf("%s%s", mcc, mnc);
+
+                if (strcmp(mccmnc, parse_context->mccmnc) == 0)
+                    parse_context->mccmnc_matched = TRUE;
+                g_free(mccmnc);
+                break;
+            }
+        }
+    } else if (strcmp(name, "apn") == 0) {
+        parse_context->found_internet_apn = FALSE;
+        nm_clear_g_free(&parse_context->apn);
+        nm_clear_g_free(&parse_context->username);
+        nm_clear_g_free(&parse_context->password);
+        nm_clear_g_free(&parse_context->gateway);
+        nm_clear_g_free(&parse_context->auth_method);
+        g_slist_free_full(parse_context->dns, g_free);
+        parse_context->dns = NULL;
+
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "value") == 0) {
+                parse_context->state = PARSER_METHOD_GSM_APN;
+                parse_context->apn   = g_strstrip(g_strdup(attribute_values[i]));
+                break;
+            }
+        }
+    }
+}
+
+static void
+parser_gsm_apn_start(ParseContext *parse_context,
+                     const char *  name,
+                     const char ** attribute_names,
+                     const char ** attribute_values)
+{
+    int i;
+
+    if (strcmp(name, "usage") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if ((strcmp(attribute_names[i], "type") == 0)
+                && (strcmp(attribute_values[i], "internet") == 0)) {
+                parse_context->found_internet_apn = TRUE;
+                break;
+            }
+        }
+    } else if (strcmp(name, "authentication") == 0) {
+        for (i = 0; attribute_names && attribute_names[i]; i++) {
+            if (strcmp(attribute_names[i], "method") == 0) {
+                nm_clear_g_free(&parse_context->auth_method);
+                parse_context->auth_method = g_strstrip(g_strdup(attribute_values[i]));
+                break;
+            }
+        }
+    }
+}
+
+static void
+parser_start_element(GMarkupParseContext *context,
+                     const char *         element_name,
+                     const char **        attribute_names,
+                     const char **        attribute_values,
+                     gpointer             user_data,
+                     GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    nm_clear_g_free(&parse_context->text_buffer);
+
+    switch (parse_context->state) {
+    case PARSER_TOPLEVEL:
+        parser_toplevel_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_COUNTRY:
+        parser_country_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_PROVIDER:
+        parser_provider_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_GSM:
+        parser_gsm_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_GSM_APN:
+        parser_gsm_apn_start(parse_context, element_name, attribute_names, attribute_values);
+        break;
+    case PARSER_METHOD_CDMA:
+        break;
+    case PARSER_ERROR:
+        break;
+    case PARSER_DONE:
+        break;
+    }
+}
+
+static void
+parser_country_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "country") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_TOPLEVEL;
+    }
+}
+
+static void
+parser_provider_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "provider") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_COUNTRY;
+    }
+}
+
+static void
+parser_gsm_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "gsm") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_PROVIDER;
+    }
+}
+
+static void
+parser_gsm_apn_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "username") == 0) {
+        nm_clear_g_free(&parse_context->username);
+        parse_context->username = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "password") == 0) {
+        nm_clear_g_free(&parse_context->password);
+        parse_context->password = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "dns") == 0) {
+        parse_context->dns =
+            g_slist_prepend(parse_context->dns, g_steal_pointer(&parse_context->text_buffer));
+    } else if (strcmp(name, "gateway") == 0) {
+        nm_clear_g_free(&parse_context->gateway);
+        parse_context->gateway = g_steal_pointer(&parse_context->text_buffer);
+    } else if (strcmp(name, "apn") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+
+        if (parse_context->mccmnc_matched && parse_context->found_internet_apn)
+            parse_context->state = PARSER_DONE;
+        else
+            parse_context->state = PARSER_METHOD_GSM;
+    }
+}
+
+static void
+parser_cdma_end(ParseContext *parse_context, const char *name)
+{
+    if (strcmp(name, "cdma") == 0) {
+        nm_clear_g_free(&parse_context->text_buffer);
+        parse_context->state = PARSER_PROVIDER;
+    }
+}
+
+static void
+parser_end_element(GMarkupParseContext *context,
+                   const char *         element_name,
+                   gpointer             user_data,
+                   GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    switch (parse_context->state) {
+    case PARSER_TOPLEVEL:
+        break;
+    case PARSER_COUNTRY:
+        parser_country_end(parse_context, element_name);
+        break;
+    case PARSER_PROVIDER:
+        parser_provider_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_GSM:
+        parser_gsm_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_GSM_APN:
+        parser_gsm_apn_end(parse_context, element_name);
+        break;
+    case PARSER_METHOD_CDMA:
+        parser_cdma_end(parse_context, element_name);
+        break;
+    case PARSER_ERROR:
+        break;
+    case PARSER_DONE:
+        break;
+    }
+}
+
+static void
+parser_text(GMarkupParseContext *context,
+            const char *         text,
+            gsize                text_len,
+            gpointer             user_data,
+            GError **            error)
+{
+    ParseContext *parse_context = user_data;
+
+    g_free(parse_context->text_buffer);
+    parse_context->text_buffer = g_strdup(text);
+}
+
+static const GMarkupParser parser = {
+    .start_element = parser_start_element,
+    .end_element   = parser_end_element,
+    .text          = parser_text,
+    .passthrough   = NULL,
+    .error         = NULL,
+};
+
+/*****************************************************************************/
+
+static void
+finish_parse_context(ParseContext *parse_context, GError *error)
+{
+    if (parse_context->callback) {
+        if (error) {
+            parse_context
+                ->callback(NULL, NULL, NULL, NULL, NULL, NULL, error, parse_context->user_data);
+        } else {
+            parse_context->callback(parse_context->apn,
+                                    parse_context->username,
+                                    parse_context->password,
+                                    parse_context->gateway,
+                                    parse_context->auth_method,
+                                    parse_context->dns,
+                                    error,
+                                    parse_context->user_data);
+        }
+    }
+
+    g_free(parse_context->mccmnc);
+    g_markup_parse_context_free(parse_context->ctx);
+
+    g_free(parse_context->text_buffer);
+    g_free(parse_context->apn);
+    g_free(parse_context->username);
+    g_free(parse_context->password);
+    g_free(parse_context->gateway);
+    g_free(parse_context->auth_method);
+    g_slist_free_full(parse_context->dns, g_free);
+
+    g_slice_free(ParseContext, parse_context);
+}
+
+static void read_next_chunk(GInputStream *stream, ParseContext *parse_context);
+
+static void
+stream_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GInputStream *stream        = G_INPUT_STREAM(source_object);
+    ParseContext *parse_context = user_data;
+    gssize        len;
+    GError *      error = NULL;
+
+    len = g_input_stream_read_finish(stream, res, &error);
+    if (len == -1) {
+        g_prefix_error(&error, "Error reading service provider database: ");
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (len == 0) {
+        g_set_error(&error,
+                    NM_UTILS_ERROR,
+                    NM_UTILS_ERROR_UNKNOWN,
+                    "Operator ID '%s' not found in service provider database",
+                    parse_context->mccmnc);
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (!g_markup_parse_context_parse(parse_context->ctx, parse_context->buffer, len, &error)) {
+        g_prefix_error(&error, "Error parsing service provider database: ");
+        finish_parse_context(parse_context, error);
+        g_clear_error(&error);
+        return;
+    }
+
+    if (parse_context->state == PARSER_DONE) {
+        finish_parse_context(parse_context, NULL);
+        return;
+    }
+
+    read_next_chunk(stream, parse_context);
+}
+
+static void
+read_next_chunk(GInputStream *stream, ParseContext *parse_context)
+{
+    g_input_stream_read_async(stream,
+                              parse_context->buffer,
+                              sizeof(parse_context->buffer),
+                              G_PRIORITY_DEFAULT,
+                              parse_context->cancellable,
+                              stream_read_cb,
+                              parse_context);
+}
+
+static void
+file_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    GFile *           file          = G_FILE(source_object);
+    ParseContext *    parse_context = user_data;
+    GFileInputStream *stream;
+    gs_free_error GError *error = NULL;
+
+    stream = g_file_read_finish(file, res, &error);
+    if (!stream) {
+        g_prefix_error(&error, "Error opening service provider database: ");
+        finish_parse_context(parse_context, error);
+        return;
+    }
+
+    read_next_chunk(G_INPUT_STREAM(stream), parse_context);
+
+    g_object_unref(stream);
+}
+
+/*****************************************************************************/
+
+void
+nm_service_providers_find_gsm_apn(const char *                     service_providers,
+                                  const char *                     mccmnc,
+                                  GCancellable *                   cancellable,
+                                  NMServiceProvidersGsmApnCallback callback,
+                                  gpointer                         user_data)
+{
+    GFile *       file;
+    ParseContext *parse_context;
+
+    parse_context              = g_slice_new0(ParseContext);
+    parse_context->mccmnc      = g_strdup(mccmnc);
+    parse_context->cancellable = cancellable;
+    parse_context->callback    = callback;
+    parse_context->user_data   = user_data;
+    parse_context->ctx         = g_markup_parse_context_new(&parser, 0, parse_context, NULL);
+
+    file = g_file_new_for_path(service_providers);
+
+    g_file_read_async(file, G_PRIORITY_DEFAULT, cancellable, file_read_cb, parse_context);
+
+    g_object_unref(file);
+}
diff --git a/src/core/devices/wwan/nm-service-providers.h b/src/core/devices/wwan/nm-service-providers.h
new file mode 100644
index 00000000..959f660a
--- /dev/null
+++ b/src/core/devices/wwan/nm-service-providers.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_SERVICE_PROVIDERS_H__
+#define __NETWORKMANAGER_SERVICE_PROVIDERS_H__
+
+typedef void (*NMServiceProvidersGsmApnCallback)(const char *  apn,
+                                                 const char *  username,
+                                                 const char *  password,
+                                                 const char *  gateway,
+                                                 const char *  auth_method,
+                                                 const GSList *dns,
+                                                 GError *      error,
+                                                 gpointer      user_data);
+
+void nm_service_providers_find_gsm_apn(const char *                     service_providers,
+                                       const char *                     mccmnc,
+                                       GCancellable *                   cancellable,
+                                       NMServiceProvidersGsmApnCallback callback,
+                                       gpointer                         user_data);
+
+#endif /* __NETWORKMANAGER_SERVICE_PROVIDERS_H__ */
diff --git a/src/core/devices/wwan/nm-wwan-factory.c b/src/core/devices/wwan/nm-wwan-factory.c
new file mode 100644
index 00000000..5d2ce2b3
--- /dev/null
+++ b/src/core/devices/wwan/nm-wwan-factory.c
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <gmodule.h>
+
+#include "devices/nm-device-factory.h"
+#include "nm-setting-gsm.h"
+#include "nm-setting-cdma.h"
+#include "nm-modem-manager.h"
+#include "nm-device-modem.h"
+#include "platform/nm-platform.h"
+
+/*****************************************************************************/
+
+#define NM_TYPE_WWAN_FACTORY (nm_wwan_factory_get_type())
+#define NM_WWAN_FACTORY(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactory))
+#define NM_WWAN_FACTORY_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass))
+#define NM_IS_WWAN_FACTORY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_WWAN_FACTORY))
+#define NM_IS_WWAN_FACTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_WWAN_FACTORY))
+#define NM_WWAN_FACTORY_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_WWAN_FACTORY, NMWwanFactoryClass))
+
+typedef struct {
+    NMModemManager *mm;
+} NMWwanFactoryPrivate;
+
+typedef struct {
+    NMDeviceFactory      parent;
+    NMWwanFactoryPrivate _priv;
+} NMWwanFactory;
+
+typedef struct {
+    NMDeviceFactoryClass parent;
+} NMWwanFactoryClass;
+
+static GType nm_wwan_factory_get_type(void);
+
+G_DEFINE_TYPE(NMWwanFactory, nm_wwan_factory, NM_TYPE_DEVICE_FACTORY)
+
+#define NM_WWAN_FACTORY_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMWwanFactory, NM_IS_WWAN_FACTORY)
+
+/*****************************************************************************/
+
+NM_DEVICE_FACTORY_DECLARE_TYPES(NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(
+    NM_LINK_TYPE_WWAN_NET) NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_GSM_SETTING_NAME,
+                                                                   NM_SETTING_CDMA_SETTING_NAME))
+
+G_MODULE_EXPORT NMDeviceFactory *
+                nm_device_factory_create(GError **error)
+{
+    return g_object_new(NM_TYPE_WWAN_FACTORY, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+modem_added_cb(NMModemManager *manager, NMModem *modem, gpointer user_data)
+{
+    NMWwanFactory * self             = NM_WWAN_FACTORY(user_data);
+    gs_unref_object NMDevice *device = NULL;
+    const char *              driver;
+
+    if (nm_modem_is_claimed(modem))
+        return;
+
+    driver = nm_modem_get_driver(modem);
+
+    /* If it was a Bluetooth modem and no bluetooth device claimed it, ignore
+     * it.  The rfcomm port (and thus the modem) gets created automatically
+     * by the Bluetooth code during the connection process.
+     */
+    if (driver && strstr(driver, "bluetooth")) {
+        nm_log_dbg(
+            LOGD_MB,
+            "WWAN factory ignores bluetooth modem '%s' which should be handled by bluetooth plugin",
+            nm_modem_get_control_port(modem));
+        return;
+    }
+
+    /* Make the new modem device */
+    device = nm_device_modem_new(modem);
+    g_signal_emit_by_name(self, NM_DEVICE_FACTORY_DEVICE_ADDED, device);
+}
+
+static NMDevice *
+create_device(NMDeviceFactory *     factory,
+              const char *          iface,
+              const NMPlatformLink *plink,
+              NMConnection *        connection,
+              gboolean *            out_ignore)
+{
+    g_return_val_if_fail(plink, NULL);
+    g_return_val_if_fail(plink->type == NM_LINK_TYPE_WWAN_NET, NULL);
+    *out_ignore = TRUE;
+    return NULL;
+}
+
+static void
+start(NMDeviceFactory *factory)
+{
+    NMWwanFactory *       self = NM_WWAN_FACTORY(factory);
+    NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self);
+
+    priv->mm = g_object_ref(nm_modem_manager_get());
+
+    g_signal_connect(priv->mm, NM_MODEM_MANAGER_MODEM_ADDED, G_CALLBACK(modem_added_cb), self);
+}
+
+/*****************************************************************************/
+
+static void
+nm_wwan_factory_init(NMWwanFactory *self)
+{}
+
+static void
+dispose(GObject *object)
+{
+    NMWwanFactory *       self = NM_WWAN_FACTORY(object);
+    NMWwanFactoryPrivate *priv = NM_WWAN_FACTORY_GET_PRIVATE(self);
+
+    if (priv->mm)
+        g_signal_handlers_disconnect_by_func(priv->mm, modem_added_cb, self);
+    g_clear_object(&priv->mm);
+
+    /* Chain up to the parent class */
+    G_OBJECT_CLASS(nm_wwan_factory_parent_class)->dispose(object);
+}
+
+static void
+nm_wwan_factory_class_init(NMWwanFactoryClass *klass)
+{
+    GObjectClass *        object_class  = G_OBJECT_CLASS(klass);
+    NMDeviceFactoryClass *factory_class = NM_DEVICE_FACTORY_CLASS(klass);
+
+    object_class->dispose = dispose;
+
+    factory_class->get_supported_types = get_supported_types;
+    factory_class->create_device       = create_device;
+    factory_class->start               = start;
+}
diff --git a/src/core/devices/wwan/tests/test-service-providers.c b/src/core/devices/wwan/tests/test-service-providers.c
new file mode 100644
index 00000000..f95cccf8
--- /dev/null
+++ b/src/core/devices/wwan/tests/test-service-providers.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019 Red Hat
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-service-providers.h"
+
+#include "nm-test-utils-core.h"
+
+static void
+test_positive_cb(const char *  apn,
+                 const char *  username,
+                 const char *  password,
+                 const char *  gateway,
+                 const char *  auth_method,
+                 const GSList *dns,
+                 GError *      error,
+                 gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_no_error(error);
+    g_assert_cmpstr(apn, ==, "gprs.example.com");
+    g_assert_cmpstr(username, ==, "praise");
+    g_assert_cmpstr(password, ==, "santa");
+    g_assert_cmpstr(gateway, ==, "192.0.2.3");
+    g_assert_cmpstr(auth_method, ==, "pap");
+
+    g_assert_nonnull(dns);
+    g_assert_cmpstr(dns->data, ==, "192.0.2.2");
+    dns = dns->next;
+    g_assert_nonnull(dns);
+    g_assert_cmpstr(dns->data, ==, "192.0.2.1");
+    g_assert_null(dns->next);
+}
+
+static void
+test_positive(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR
+                                      "/src/core/devices/wwan/tests/test-service-providers.xml",
+                                      "13337",
+                                      NULL,
+                                      test_positive_cb,
+                                      loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+static void
+test_negative_cb(const char *  apn,
+                 const char *  username,
+                 const char *  password,
+                 const char *  gateway,
+                 const char *  auth_method,
+                 const GSList *dns,
+                 GError *      error,
+                 gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_error(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN);
+}
+
+static void
+test_negative(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn(NM_BUILD_SRCDIR
+                                      "/src/core/devices/wwan/tests/test-service-providers.xml",
+                                      "78130",
+                                      NULL,
+                                      test_negative_cb,
+                                      loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+static void
+test_nonexistent_cb(const char *  apn,
+                    const char *  username,
+                    const char *  password,
+                    const char *  gateway,
+                    const char *  auth_method,
+                    const GSList *dns,
+                    GError *      error,
+                    gpointer      user_data)
+{
+    GMainLoop *loop = user_data;
+
+    g_main_loop_quit(loop);
+    g_assert_error(error, G_IO_ERROR, G_IO_ERROR_AGAIN);
+}
+
+static void
+test_nonexistent(void)
+{
+    GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+    nm_service_providers_find_gsm_apn("nonexistent.xml", "13337", NULL, test_nonexistent_cb, loop);
+    g_main_loop_run(loop);
+    g_main_loop_unref(loop);
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT");
+
+    g_test_add_func("/service-providers/positive", test_positive);
+    g_test_add_func("/service-providers/negative", test_negative);
+    g_test_add_func("/service-providers/nonexistent", test_nonexistent);
+
+    return g_test_run();
+}
diff --git a/src/core/devices/wwan/tests/test-service-providers.xml b/src/core/devices/wwan/tests/test-service-providers.xml
new file mode 100644
index 00000000..f0ca2deb
--- /dev/null
+++ b/src/core/devices/wwan/tests/test-service-providers.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding='utf-8'?>
+<!DOCTYPE serviceproviders SYSTEM "serviceproviders.2.dtd">
+
+<serviceproviders format="2.0">
+
+<country code="feh">
+	<provider>
+		<name>Sophia</name>
+		<gsm>
+			<network-id mcc="666" mnc="999"/>
+			<apn value="access.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>APN</name>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+			</apn>
+		</gsm>
+	</provider>
+</country>
+
+<country code="meh">
+	<provider>
+		<name>Demiurge</name>
+		<gsm>
+			<network-id mcc="133" mnc="37"/>
+			<network-id mcc="133" mnc="666"/>
+			<apn value="mms">
+				<usage type="mms"/>
+				<name>Unsolicited Nudes MMS</name>
+				<username>mms</username>
+				<password>mms</password>
+				<mmsc>http://mms.example.com/</mmsc>
+				<mmsproxy>192.0.2.1:8080</mmsproxy>
+			</apn>
+			<apn value="gprs.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>GPRS</name>
+				<username>praise</username>
+				<password>santa</password>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+				<gateway>192.0.2.3</gateway>
+				<authentication method="pap"/>
+			</apn>
+			<apn value="second.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>Second</name>
+				<username>worship</username>
+				<password>doom</password>
+			</apn>
+		</gsm>
+	</provider>
+
+	<provider>
+		<name>Personal</name>
+		<gsm>
+			<network-id mcc="666" mnc="999"/>
+			<apn value="access.example.com">
+				<plan type="postpaid"/>
+				<usage type="internet"/>
+				<name>APN</name>
+				<dns>192.0.2.1</dns>
+				<dns>192.0.2.2</dns>
+			</apn>
+		</gsm>
+	</provider>
+</country>
+
+</serviceproviders>
+