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/nm-atm-manager.c3
-rw-r--r--src/core/devices/adsl/nm-device-adsl.c12
-rw-r--r--src/core/devices/adsl/nm-device-adsl.h3
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.c5
-rw-r--r--src/core/devices/bluetooth/nm-bluez-manager.h2
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.h2
-rw-r--r--src/core/devices/nm-device-6lowpan.c39
-rw-r--r--src/core/devices/nm-device-6lowpan.h2
-rw-r--r--src/core/devices/nm-device-bond.c140
-rw-r--r--src/core/devices/nm-device-bond.h3
-rw-r--r--src/core/devices/nm-device-bridge.c11
-rw-r--r--src/core/devices/nm-device-bridge.h2
-rw-r--r--src/core/devices/nm-device-dummy.c2
-rw-r--r--src/core/devices/nm-device-dummy.h2
-rw-r--r--src/core/devices/nm-device-ethernet.c15
-rw-r--r--src/core/devices/nm-device-ethernet.h2
-rw-r--r--src/core/devices/nm-device-factory.c1
-rw-r--r--src/core/devices/nm-device-factory.h5
-rw-r--r--src/core/devices/nm-device-generic.h2
-rw-r--r--src/core/devices/nm-device-infiniband.c8
-rw-r--r--src/core/devices/nm-device-infiniband.h2
-rw-r--r--src/core/devices/nm-device-ip-tunnel.c277
-rw-r--r--src/core/devices/nm-device-ip-tunnel.h3
-rw-r--r--src/core/devices/nm-device-logging.h13
-rw-r--r--src/core/devices/nm-device-loopback.c166
-rw-r--r--src/core/devices/nm-device-loopback.h27
-rw-r--r--src/core/devices/nm-device-macsec.c39
-rw-r--r--src/core/devices/nm-device-macsec.h2
-rw-r--r--src/core/devices/nm-device-macvlan.c39
-rw-r--r--src/core/devices/nm-device-macvlan.h2
-rw-r--r--src/core/devices/nm-device-ppp.c2
-rw-r--r--src/core/devices/nm-device-ppp.h2
-rw-r--r--src/core/devices/nm-device-tun.c2
-rw-r--r--src/core/devices/nm-device-tun.h2
-rw-r--r--src/core/devices/nm-device-utils.c42
-rw-r--r--src/core/devices/nm-device-veth.c2
-rw-r--r--src/core/devices/nm-device-veth.h3
-rw-r--r--src/core/devices/nm-device-vlan.c58
-rw-r--r--src/core/devices/nm-device-vlan.h3
-rw-r--r--src/core/devices/nm-device-vrf.c2
-rw-r--r--src/core/devices/nm-device-vrf.h2
-rw-r--r--src/core/devices/nm-device-vxlan.c30
-rw-r--r--src/core/devices/nm-device-vxlan.h2
-rw-r--r--src/core/devices/nm-device-wireguard.c25
-rw-r--r--src/core/devices/nm-device-wireguard.h2
-rw-r--r--src/core/devices/nm-device-wpan.c2
-rw-r--r--src/core/devices/nm-device-wpan.h3
-rw-r--r--src/core/devices/nm-device.c353
-rw-r--r--src/core/devices/nm-device.h28
-rw-r--r--src/core/devices/nm-lldp-listener.c332
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c16
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.h2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.h2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.h2
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c3
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c581
-rw-r--r--src/core/devices/ovs/nm-ovsdb.h18
-rw-r--r--src/core/devices/team/nm-device-team.c62
-rw-r--r--src/core/devices/team/nm-device-team.h3
-rw-r--r--src/core/devices/team/nm-team-factory.c2
-rw-r--r--src/core/devices/tests/test-lldp.c16
-rw-r--r--src/core/devices/wifi/nm-device-iwd-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c466
-rw-r--r--src/core/devices/wifi/nm-device-iwd.h4
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c1
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.h2
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c49
-rw-r--r--src/core/devices/wifi/nm-device-wifi.h3
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c261
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.h7
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-factory.c2
-rw-r--r--src/core/devices/wifi/nm-wifi-p2p-peer.h2
-rw-r--r--src/core/devices/wifi/nm-wifi-utils.c175
-rw-r--r--src/core/devices/wwan/nm-device-modem.c24
-rw-r--r--src/core/devices/wwan/nm-device-modem.h2
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c13
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.h2
-rw-r--r--src/core/devices/wwan/nm-modem-manager.h2
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.c740
-rw-r--r--src/core/devices/wwan/nm-modem-ofono.h5
-rw-r--r--src/core/devices/wwan/nm-modem.c10
-rw-r--r--src/core/devices/wwan/nm-modem.h2
-rw-r--r--src/core/devices/wwan/nm-wwan-factory.c2
85 files changed, 2895 insertions, 1320 deletions
diff --git a/src/core/devices/adsl/nm-atm-manager.c b/src/core/devices/adsl/nm-atm-manager.c
index cc8d37e6..f3c0d367 100644
--- a/src/core/devices/adsl/nm-atm-manager.c
+++ b/src/core/devices/adsl/nm-atm-manager.c
@@ -17,7 +17,8 @@
 /*****************************************************************************/
 
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c
index 79705c28..fcd16b1c 100644
--- a/src/core/devices/adsl/nm-device-adsl.c
+++ b/src/core/devices/adsl/nm-device-adsl.c
@@ -214,12 +214,12 @@ error:
 }
 
 static void
-link_changed_cb(NMPlatform     *platform,
-                int             obj_type_i,
-                int             ifindex,
-                NMPlatformLink *info,
-                int             change_type_i,
-                NMDeviceAdsl   *self)
+link_changed_cb(NMPlatform           *platform,
+                int                   obj_type_i,
+                int                   ifindex,
+                const NMPlatformLink *info,
+                int                   change_type_i,
+                NMDeviceAdsl         *self)
 {
     const NMPlatformSignalChangeType change_type = change_type_i;
 
diff --git a/src/core/devices/adsl/nm-device-adsl.h b/src/core/devices/adsl/nm-device-adsl.h
index c5c18901..287a1401 100644
--- a/src/core/devices/adsl/nm-device-adsl.h
+++ b/src/core/devices/adsl/nm-device-adsl.h
@@ -10,7 +10,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.c b/src/core/devices/bluetooth/nm-bluez-manager.c
index 6bc00faf..ac26ede8 100644
--- a/src/core/devices/bluetooth/nm-bluez-manager.c
+++ b/src/core/devices/bluetooth/nm-bluez-manager.c
@@ -1010,8 +1010,9 @@ _network_server_get_bluez_manager(const NMBtVTableNetworkServer *vtable_network_
 {
     NMBluezManager *self;
 
-    self = (NMBluezManager *) (((char *) vtable_network_server)
-                               - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server));
+    self = NM_CAST_ALIGN(NMBluezManager,
+                         (((char *) vtable_network_server)
+                          - G_STRUCT_OFFSET(NMBluezManager, _priv.vtable_network_server)));
 
     g_return_val_if_fail(NM_IS_BLUEZ_MANAGER(self), NULL);
 
diff --git a/src/core/devices/bluetooth/nm-bluez-manager.h b/src/core/devices/bluetooth/nm-bluez-manager.h
index 299f5c3b..7c823c64 100644
--- a/src/core/devices/bluetooth/nm-bluez-manager.h
+++ b/src/core/devices/bluetooth/nm-bluez-manager.h
@@ -8,7 +8,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/bluetooth/nm-device-bt.h b/src/core/devices/bluetooth/nm-device-bt.h
index 59457fba..bf17e91f 100644
--- a/src/core/devices/bluetooth/nm-device-bt.h
+++ b/src/core/devices/bluetooth/nm-device-bt.h
@@ -9,7 +9,7 @@
 #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(obj) (_NM_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))
diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c
index d6543356..870a1c14 100644
--- a/src/core/devices/nm-device-6lowpan.c
+++ b/src/core/devices/nm-device-6lowpan.c
@@ -21,7 +21,6 @@
 /*****************************************************************************/
 
 typedef struct {
-    gulong parent_state_id;
 } NMDevice6LowpanPrivate;
 
 struct _NMDevice6Lowpan {
@@ -41,51 +40,15 @@ G_DEFINE_TYPE(NMDevice6Lowpan, nm_device_6lowpan, NM_TYPE_DEVICE)
 /*****************************************************************************/
 
 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,
@@ -272,7 +235,7 @@ nm_device_6lowpan_class_init(NMDevice6LowpanClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_6LOWPAN_DEVICE_FACTORY, NM6LowpanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-6lowpan.h b/src/core/devices/nm-device-6lowpan.h
index 34ea3c8d..bc6f77c4 100644
--- a/src/core/devices/nm-device-6lowpan.h
+++ b/src/core/devices/nm-device-6lowpan.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 10765b60..0b0697e3 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -20,6 +20,7 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "nm-manager.h"
 #include "nm-setting-bond-port.h"
+#include "nm-bond-manager.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceBond
 #include "nm-device-logging.h"
@@ -59,7 +60,8 @@
 /*****************************************************************************/
 
 struct _NMDeviceBond {
-    NMDevice parent;
+    NMDevice       parent;
+    NMBondManager *bond_manager;
 };
 
 struct _NMDeviceBondClass {
@@ -178,7 +180,9 @@ update_connection(NMDevice *device, NMConnection *connection)
         gs_free char *value  = NULL;
         char         *p;
 
-        if (NM_IN_STRSET(option, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE))
+        if (NM_IN_STRSET(option,
+                         NM_SETTING_BOND_OPTION_ACTIVE_SLAVE,
+                         NM_SETTING_BOND_OPTION_BALANCE_SLB))
             continue;
 
         value =
@@ -224,9 +228,10 @@ controller_update_port_connection(NMDevice     *self,
                                   GError      **error)
 {
     NMSettingBondPort *s_port;
-    int                ifindex_port = nm_device_get_ifindex(port);
-    uint               queue_id     = NM_BOND_PORT_QUEUE_ID_DEF;
-    gs_free char      *queue_id_str = NULL;
+    int                ifindex_port       = nm_device_get_ifindex(port);
+    NMConnection      *applied_connection = nm_device_get_applied_connection(self);
+    uint               queue_id           = NM_BOND_PORT_QUEUE_ID_DEF;
+    gs_free char      *queue_id_str       = NULL;
 
     g_return_val_if_fail(ifindex_port > 0, FALSE);
 
@@ -243,7 +248,7 @@ controller_update_port_connection(NMDevice     *self,
 
     g_object_set(nm_connection_get_setting_connection(connection),
                  NM_SETTING_CONNECTION_MASTER,
-                 nm_device_get_iface(self),
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_BOND_SETTING_NAME,
                  NULL);
@@ -274,10 +279,10 @@ set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new
             in_addr_t   a4;
 
             s = new_strv[i];
-            if (nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &a4)) {
+            if (nm_inet_parse_bin(AF_INET, s, NULL, &a4)) {
                 char sbuf[INET_ADDRSTRLEN];
 
-                _nm_utils_inet4_ntop(a4, sbuf);
+                nm_inet4_ntop(a4, sbuf);
                 if (!nm_streq(s, sbuf)) {
                     if (!free_list)
                         free_list = g_ptr_array_new_with_free_func(g_free);
@@ -368,7 +373,7 @@ _bond_arp_ip_target_to_platform(const char *value, in_addr_t out[static NM_BOND_
     for (i = 0; ip[i]; i++) {
         if (added > NM_BOND_MAX_ARP_TARGETS - 1)
             break;
-        if (!nm_utils_parse_inaddr_bin(AF_INET, ip[i], NULL, &in_a))
+        if (!nm_inet_parse_bin(AF_INET, ip[i], NULL, &in_a))
             nm_assert_not_reached(); /* verify() already validated the IP addresses */
 
         out[added++] = in_a;
@@ -460,10 +465,97 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
     props->tlb_dynamic_lb_has   = NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB);
 }
 
+static void
+_balance_slb_cb(NMBondManager *bond_manager, NMBondManagerEventType event_type, gpointer user_data)
+{
+    NMDevice     *device = user_data;
+    NMDeviceBond *self   = NM_DEVICE_BOND(device);
+
+    nm_assert(NM_IS_DEVICE_BOND(self));
+    nm_assert(self->bond_manager == bond_manager);
+
+    switch (event_type) {
+    case NM_BOND_MANAGER_EVENT_TYPE_STATE:
+        switch (nm_bond_manager_get_state(bond_manager)) {
+        case NM_OPTION_BOOL_FALSE:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED) {
+                _LOGD(LOGD_BOND, "balance-slb: failed");
+                nm_device_state_changed(device,
+                                        NM_DEVICE_STATE_FAILED,
+                                        NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            }
+            return;
+        case NM_OPTION_BOOL_TRUE:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED
+                && nm_device_devip_get_state(device, AF_UNSPEC) <= NM_DEVICE_IP_STATE_PENDING) {
+                nm_device_devip_set_state(device, AF_UNSPEC, NM_DEVICE_IP_STATE_READY, NULL);
+            }
+            return;
+        case NM_OPTION_BOOL_DEFAULT:
+            if (nm_device_get_state(device) <= NM_DEVICE_STATE_ACTIVATED
+                && nm_device_devip_get_state(device, AF_UNSPEC) == NM_DEVICE_IP_STATE_READY) {
+                /* We are again busy. We can also go back to "pending" from "ready".
+                 * If ip-config state is not yet complete, this will further delay it.
+                 * Otherwise, it should have no effect. */
+                nm_device_devip_set_state(device, AF_UNSPEC, NM_DEVICE_IP_STATE_PENDING, NULL);
+            }
+            return;
+        }
+        nm_assert_not_reached();
+        return;
+    }
+
+    nm_assert_not_reached();
+}
+
+static void
+_balance_slb_setup(NMDeviceBond *self, NMConnection *connection)
+{
+    int            ifindex     = nm_device_get_ifindex(NM_DEVICE(self));
+    gboolean       balance_slb = FALSE;
+    const char    *uuid;
+    NMSettingBond *s_bond;
+
+    if (ifindex > 0 && connection && (s_bond = nm_connection_get_setting_bond(connection)))
+        balance_slb = _v_intbool(s_bond, NM_SETTING_BOND_OPTION_BALANCE_SLB);
+
+    if (!balance_slb) {
+        if (nm_clear_pointer(&self->bond_manager, nm_bond_manager_destroy)) {
+            _LOGD(LOGD_BOND, "balance-slb: stopped");
+            nm_device_devip_set_state(NM_DEVICE(self), AF_UNSPEC, NM_DEVICE_IP_STATE_NONE, NULL);
+        }
+        return;
+    }
+
+    uuid = nm_connection_get_uuid(connection);
+
+    if (self->bond_manager) {
+        if (nm_bond_manager_get_ifindex(self->bond_manager) == ifindex
+            && nm_streq0(nm_bond_manager_get_connection_uuid(self->bond_manager), uuid)) {
+            _LOGD(LOGD_BOND, "balance-slb: reapply");
+            nm_bond_manager_reapply(self->bond_manager);
+            return;
+        }
+        nm_clear_pointer(&self->bond_manager, nm_bond_manager_destroy);
+        _LOGD(LOGD_BOND, "balance-slb: restart");
+    }
+
+    _LOGD(LOGD_BOND, "balance-slb: start");
+    if (nm_device_devip_get_state(NM_DEVICE(self), AF_UNSPEC) < NM_DEVICE_IP_STATE_PENDING)
+        nm_device_devip_set_state(NM_DEVICE(self), AF_UNSPEC, NM_DEVICE_IP_STATE_PENDING, NULL);
+    self->bond_manager = nm_bond_manager_new(nm_device_get_platform(NM_DEVICE(self)),
+                                             ifindex,
+                                             uuid,
+                                             _balance_slb_cb,
+                                             self);
+    nm_assert(nm_bond_manager_get_state(self->bond_manager) == NM_OPTION_BOOL_DEFAULT);
+}
+
 static NMActStageReturn
 act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 {
-    NMActStageReturn  ret = NM_ACT_STAGE_RETURN_SUCCESS;
+    NMDeviceBond     *self = NM_DEVICE_BOND(device);
+    NMActStageReturn  ret  = NM_ACT_STAGE_RETURN_SUCCESS;
     NMConnection     *connection;
     NMSettingBond    *s_bond;
     NMPlatformLnkBond props;
@@ -476,6 +568,14 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
     s_bond = nm_connection_get_setting_bond(connection);
     g_return_val_if_fail(s_bond, NM_ACT_STAGE_RETURN_FAILURE);
 
+    if (nm_device_sys_iface_state_is_external(device))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
+    _balance_slb_setup(self, connection);
+
+    if (nm_device_sys_iface_state_is_external_or_assume(device))
+        return NM_ACT_STAGE_RETURN_SUCCESS;
+
     _platform_lnk_bond_init_from_setting(s_bond, &props);
 
     /* Interface must be down to set bond options */
@@ -684,7 +784,7 @@ can_reapply_change(NMDevice   *device,
             const char *name = *option_list;
 
             /* We support changes to these */
-            if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL))
+            if (NM_IN_STRSET(name, OPTIONS_REAPPLY_FULL, NM_SETTING_BOND_OPTION_BALANCE_SLB))
                 continue;
 
             /* Reject any other changes */
@@ -730,6 +830,16 @@ reapply_connection(NMDevice *device, NMConnection *con_old, NMConnection *con_ne
     set_bond_arp_ip_targets(device, s_bond);
 
     set_bond_attrs_or_default(device, s_bond, NM_MAKE_STRV(OPTIONS_REAPPLY_SUBSET));
+
+    _balance_slb_setup(self, con_new);
+}
+
+static void
+deactivate(NMDevice *device)
+{
+    NMDeviceBond *self = NM_DEVICE_BOND(device);
+
+    _balance_slb_setup(self, NULL);
 }
 
 /*****************************************************************************/
@@ -768,20 +878,22 @@ nm_device_bond_class_init(NMDeviceBondClass *klass)
     device_class->update_connection              = update_connection;
     device_class->master_update_slave_connection = controller_update_port_connection;
 
-    device_class->create_and_realize = create_and_realize;
-    device_class->act_stage1_prepare = act_stage1_prepare;
+    device_class->create_and_realize                             = create_and_realize;
+    device_class->act_stage1_prepare                             = act_stage1_prepare;
+    device_class->act_stage1_prepare_also_for_external_or_assume = TRUE;
     device_class->get_configured_mtu = nm_device_get_configured_mtu_for_wired;
     device_class->attach_port        = attach_port;
     device_class->detach_port        = detach_port;
     device_class->can_reapply_change = can_reapply_change;
     device_class->reapply_connection = reapply_connection;
+    device_class->deactivate         = deactivate;
 }
 
 /*****************************************************************************/
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BOND_DEVICE_FACTORY, NMBondDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-bond.h b/src/core/devices/nm-device-bond.h
index 19301793..083189bb 100644
--- a/src/core/devices/nm-device-bond.h
+++ b/src/core/devices/nm-device-bond.h
@@ -9,7 +9,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index 31cf361e..c5ce34c2 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -679,9 +679,10 @@ master_update_slave_connection(NMDevice     *device,
     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;
+    int                  ifindex_slave      = nm_device_get_ifindex(slave);
+    NMConnection        *applied_connection = nm_device_get_applied_connection(device);
+
+    const Option *option;
 
     g_return_val_if_fail(ifindex_slave > 0, FALSE);
 
@@ -717,7 +718,7 @@ master_update_slave_connection(NMDevice     *device,
 
     g_object_set(s_con,
                  NM_SETTING_CONNECTION_MASTER,
-                 iface,
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_BRIDGE_SETTING_NAME,
                  NULL);
@@ -1270,7 +1271,7 @@ nm_device_bridge_class_init(NMDeviceBridgeClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_BRIDGE_DEVICE_FACTORY, NMBridgeDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-bridge.h b/src/core/devices/nm-device-bridge.h
index 32437947..6d9f1614 100644
--- a/src/core/devices/nm-device-bridge.h
+++ b/src/core/devices/nm-device-bridge.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-dummy.c b/src/core/devices/nm-device-dummy.c
index de701a0d..36a0ee6f 100644
--- a/src/core/devices/nm-device-dummy.c
+++ b/src/core/devices/nm-device-dummy.c
@@ -136,7 +136,7 @@ nm_device_dummy_class_init(NMDeviceDummyClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DUMMY_DEVICE_FACTORY, NMDummyDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-dummy.h b/src/core/devices/nm-device-dummy.h
index 2845b4eb..9e550887 100644
--- a/src/core/devices/nm-device-dummy.h
+++ b/src/core/devices/nm-device-dummy.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 4788cdc6..97cf84a1 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -1735,11 +1735,10 @@ new_default_connection(NMDevice *self)
 
     /* 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_uuid_generate_from_strings("default-wired",
-                                         nm_utils_machine_id_str(),
-                                         defname,
-                                         perm_hw_addr ?: iface,
-                                         NULL);
+    uuid = nm_uuid_generate_from_strings_old("default-wired",
+                                             nm_utils_machine_id_str(),
+                                             defname,
+                                             perm_hw_addr ?: iface);
 
     g_object_set(setting,
                  NM_SETTING_CONNECTION_ID,
@@ -2114,8 +2113,10 @@ nm_device_ethernet_class_init(NMDeviceEthernetClass *klass)
 /*****************************************************************************/
 
 #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))
+#define NM_ETHERNET_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                           \
+                                    NM_TYPE_ETHERNET_DEVICE_FACTORY, \
+                                    NMEthernetDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-ethernet.h b/src/core/devices/nm-device-ethernet.h
index 6e134d71..6c4acb82 100644
--- a/src/core/devices/nm-device-ethernet.h
+++ b/src/core/devices/nm-device-ethernet.h
@@ -11,7 +11,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index 41e9480f..2f0ae2e8 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -398,6 +398,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb
     _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_loopback_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);
diff --git a/src/core/devices/nm-device-factory.h b/src/core/devices/nm-device-factory.h
index 51d64767..ac5ae05f 100644
--- a/src/core/devices/nm-device-factory.h
+++ b/src/core/devices/nm-device-factory.h
@@ -17,7 +17,7 @@
 
 #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))
+    (_NM_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))
@@ -208,7 +208,8 @@ NMDevice *nm_device_factory_create_device(NMDeviceFactory      *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_init(NM##mixed##DeviceFactory *self)                 \
+    {}                                                                                           \
                                                                                                  \
     static void nm_##lower##_device_factory_class_init(NM##mixed##DeviceFactoryClass *klass)     \
     {                                                                                            \
diff --git a/src/core/devices/nm-device-generic.h b/src/core/devices/nm-device-generic.h
index 48c43523..f06a5bdc 100644
--- a/src/core/devices/nm-device-generic.h
+++ b/src/core/devices/nm-device-generic.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c
index 6b6aef86..d025931e 100644
--- a/src/core/devices/nm-device-infiniband.c
+++ b/src/core/devices/nm-device-infiniband.c
@@ -403,10 +403,10 @@ nm_device_infiniband_class_init(NMDeviceInfinibandClass *klass)
 /*****************************************************************************/
 
 #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))
+#define NM_INFINIBAND_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                             \
+                                    NM_TYPE_INFINIBAND_DEVICE_FACTORY, \
+                                    NMInfinibandDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-infiniband.h b/src/core/devices/nm-device-infiniband.h
index 69994a2d..5c915dd4 100644
--- a/src/core/devices/nm-device-infiniband.h
+++ b/src/core/devices/nm-device-infiniband.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-ip-tunnel.c b/src/core/devices/nm-device-ip-tunnel.c
index 02680dd7..5e5ba632 100644
--- a/src/core/devices/nm-device-ip-tunnel.c
+++ b/src/core/devices/nm-device-ip-tunnel.c
@@ -39,6 +39,7 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceIPTunnel,
                              PROP_OUTPUT_KEY,
                              PROP_ENCAPSULATION_LIMIT,
                              PROP_FLOW_LABEL,
+                             PROP_FWMARK,
                              PROP_FLAGS, );
 
 typedef struct {
@@ -53,6 +54,7 @@ typedef struct {
     char           *output_key;
     guint8          encap_limit;
     guint32         flow_label;
+    guint32         fwmark;
     NMIPTunnelFlags flags;
 } NMDeviceIPTunnelPrivate;
 
@@ -107,12 +109,12 @@ address_equal_pp(int addr_family, const char *a, const char *b)
     nm_assert_addr_family(addr_family);
 
     if (a) {
-        if (!nm_utils_parse_inaddr_bin(addr_family, a, NULL, &addr_a_val))
+        if (!nm_inet_parse_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))
+        if (!nm_inet_parse_bin(addr_family, b, NULL, &addr_b_val))
             nm_assert_not_reached();
         addr_b = &addr_b_val;
     }
@@ -125,7 +127,7 @@ 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));
+    nm_assert(!*p_addr || nm_inet_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))
@@ -136,7 +138,7 @@ address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
     if (*p_addr) {
         NMIPAddr addr_val;
 
-        if (!nm_utils_parse_inaddr_bin(addr_family, *p_addr, NULL, &addr_val))
+        if (!nm_inet_parse_bin(addr_family, *p_addr, NULL, &addr_val))
             nm_assert_not_reached();
 
         if (nm_ip_addr_equal(addr_family, &addr_val, addr_new))
@@ -145,7 +147,7 @@ address_set(int addr_family, char **p_addr, const NMIPAddr *addr_new)
         g_free(*p_addr);
     }
 
-    *p_addr = nm_utils_inet_ntop_dup(addr_family, addr_new);
+    *p_addr = nm_inet_ntop_dup(addr_family, addr_new);
     return TRUE;
 }
 
@@ -162,8 +164,10 @@ update_properties_from_ifindex(NMDevice *device, int ifindex)
     guint8                   encap_limit    = 0;
     gboolean                 pmtud          = FALSE;
     guint32                  flow_label     = 0;
+    guint32                  fwmark         = 0;
     NMIPTunnelFlags          flags          = NM_IP_TUNNEL_FLAG_NONE;
-    char                    *key;
+    gs_free char            *input_key      = NULL;
+    gs_free char            *output_key     = NULL;
 
     if (ifindex <= 0) {
 clear:
@@ -207,35 +211,10 @@ clear:
         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);
-            }
-        }
+        if (NM_FLAGS_HAS(lnk->input_flags, NM_GRE_KEY))
+            input_key = g_strdup_printf("%u", lnk->input_key);
+        if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY))
+            output_key = g_strdup_printf("%u", lnk->output_key);
     } else if (priv->mode == NM_IP_TUNNEL_MODE_SIT) {
         const NMPlatformLnkSit *lnk;
 
@@ -296,36 +275,45 @@ clear:
         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->input_flags, NM_GRE_KEY))
+                input_key = g_strdup_printf("%u", lnk->input_key);
+            if (NM_FLAGS_HAS(lnk->output_flags, NM_GRE_KEY))
+                output_key = g_strdup_printf("%u", lnk->output_key);
+        }
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_VTI) {
+        const NMPlatformLnkVti *lnk;
 
-            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);
-                }
-            }
+        lnk = nm_platform_link_get_lnk_vti(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "vti");
+            goto clear;
+        }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr4    = lnk->local;
+        remote.addr4   = lnk->remote;
+        fwmark         = lnk->fwmark;
+        if (lnk->ikey)
+            input_key = g_strdup_printf("%u", lnk->ikey);
+        if (lnk->okey)
+            input_key = g_strdup_printf("%u", lnk->okey);
+    } else if (priv->mode == NM_IP_TUNNEL_MODE_VTI6) {
+        const NMPlatformLnkVti6 *lnk;
+
+        lnk = nm_platform_link_get_lnk_vti6(nm_device_get_platform(device), ifindex, NULL);
+        if (!lnk) {
+            _LOGW(LOGD_PLATFORM, "could not read %s properties", "vti6");
+            goto clear;
         }
+
+        parent_ifindex = lnk->parent_ifindex;
+        local.addr6    = lnk->local;
+        remote.addr6   = lnk->remote;
+        fwmark         = lnk->fwmark;
+        if (lnk->ikey)
+            input_key = g_strdup_printf("%u", lnk->ikey);
+        if (lnk->okey)
+            input_key = g_strdup_printf("%u", lnk->okey);
     } else
         g_return_if_reached();
 
@@ -337,7 +325,6 @@ clear:
         _notify(self, PROP_REMOTE);
 
 out:
-
     if (priv->ttl != ttl) {
         priv->ttl = ttl;
         _notify(self, PROP_TTL);
@@ -363,6 +350,23 @@ out:
         _notify(self, PROP_FLOW_LABEL);
     }
 
+    if (!nm_streq0(priv->input_key, input_key)) {
+        g_free(priv->input_key);
+        priv->input_key = g_steal_pointer(&input_key);
+        _notify(self, PROP_INPUT_KEY);
+    }
+
+    if (!nm_streq0(priv->output_key, output_key)) {
+        g_free(priv->output_key);
+        priv->output_key = g_steal_pointer(&output_key);
+        _notify(self, PROP_OUTPUT_KEY);
+    }
+
+    if (priv->fwmark != fwmark) {
+        priv->fwmark = fwmark;
+        _notify(self, PROP_FWMARK);
+    }
+
     if (priv->flags != flags) {
         priv->flags = flags;
         _notify(self, PROP_FLAGS);
@@ -467,11 +471,17 @@ update_connection(NMDevice *device, NMConnection *connection)
                      NULL);
     }
 
+    if (nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel) != priv->fwmark) {
+        g_object_set(G_OBJECT(s_ip_tunnel), NM_SETTING_IP_TUNNEL_FWMARK, priv->fwmark, 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)) {
+                  NM_IP_TUNNEL_MODE_IP6GRETAP,
+                  NM_IP_TUNNEL_MODE_VTI,
+                  NM_IP_TUNNEL_MODE_VTI6)) {
         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,
@@ -493,6 +503,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     NMDeviceIPTunnel        *self = NM_DEVICE_IP_TUNNEL(device);
     NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
     NMSettingIPTunnel       *s_ip_tunnel;
+    NMIPTunnelMode           mode;
     const char              *parent;
 
     if (!NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)
@@ -500,8 +511,9 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
         return FALSE;
 
     s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
+    mode        = nm_setting_ip_tunnel_get_mode(s_ip_tunnel);
 
-    if (nm_setting_ip_tunnel_get_mode(s_ip_tunnel) != priv->mode) {
+    if (mode != priv->mode) {
         nm_utils_error_set_literal(error,
                                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                    "incompatible IP tunnel mode");
@@ -536,14 +548,16 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
             return FALSE;
         }
 
-        if (nm_setting_ip_tunnel_get_ttl(s_ip_tunnel) != priv->ttl) {
+        if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+            && 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) {
+        if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+            && 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");
@@ -551,11 +565,14 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
         }
 
         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");
+            if (!NM_IN_SET(mode, NM_IP_TUNNEL_MODE_VTI, NM_IP_TUNNEL_MODE_VTI6)
+                && nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel)
+                       != priv->path_mtu_discovery) {
+                nm_utils_error_set(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "MTU discovery setting of IP tunnel mismatches: %d vs %d",
+                                   nm_setting_ip_tunnel_get_path_mtu_discovery(s_ip_tunnel),
+                                   priv->path_mtu_discovery);
                 return FALSE;
             }
         } else {
@@ -605,6 +622,10 @@ platform_link_to_tunnel_mode(const NMPlatformLink *link)
         return NM_IP_TUNNEL_MODE_IPIP;
     case NM_LINK_TYPE_SIT:
         return NM_IP_TUNNEL_MODE_SIT;
+    case NM_LINK_TYPE_VTI:
+        return NM_IP_TUNNEL_MODE_VTI;
+    case NM_LINK_TYPE_VTI6:
+        return NM_IP_TUNNEL_MODE_VTI6;
     default:
         g_return_val_if_reached(NM_IP_TUNNEL_MODE_UNKNOWN);
     }
@@ -630,7 +651,9 @@ tunnel_mode_to_link_type(NMIPTunnelMode tunnel_mode)
     case NM_IP_TUNNEL_MODE_SIT:
         return NM_LINK_TYPE_SIT;
     case NM_IP_TUNNEL_MODE_VTI:
+        return NM_LINK_TYPE_VTI;
     case NM_IP_TUNNEL_MODE_VTI6:
+        return NM_LINK_TYPE_VTI6;
     case NM_IP_TUNNEL_MODE_ISATAP:
         return NM_LINK_TYPE_UNKNOWN;
     case NM_IP_TUNNEL_MODE_UNKNOWN:
@@ -654,6 +677,8 @@ create_and_realize(NMDevice              *device,
     NMPlatformLnkSit    lnk_sit    = {};
     NMPlatformLnkIpIp   lnk_ipip   = {};
     NMPlatformLnkIp6Tnl lnk_ip6tnl = {};
+    NMPlatformLnkVti    lnk_vti    = {};
+    NMPlatformLnkVti6   lnk_vti6   = {};
     const char         *str;
     gint64              val;
     NMIPTunnelMode      mode;
@@ -871,6 +896,81 @@ create_and_realize(NMDevice              *device,
             return FALSE;
         }
         break;
+    case NM_IP_TUNNEL_MODE_VTI:
+        if (parent)
+            lnk_vti.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_vti.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        nm_assert(str);
+        inet_pton(AF_INET, str, &lnk_vti.remote);
+
+        lnk_vti.ikey = _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                                    10,
+                                                    0,
+                                                    G_MAXUINT32,
+                                                    0);
+        lnk_vti.okey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti.fwmark = nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel);
+
+        r = nm_platform_link_vti_add(nm_device_get_platform(device), iface, &lnk_vti, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create VTI interface '%s' for '%s': %s",
+                        iface,
+                        nm_connection_get_id(connection),
+                        nm_strerror(r));
+            return FALSE;
+        }
+        break;
+    case NM_IP_TUNNEL_MODE_VTI6:
+        if (parent)
+            lnk_vti6.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_vti6.local);
+
+        str = nm_setting_ip_tunnel_get_remote(s_ip_tunnel);
+        nm_assert(str);
+        inet_pton(AF_INET6, str, &lnk_vti6.remote);
+
+        lnk_vti6.ikey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_input_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti6.okey =
+            _nm_utils_ascii_str_to_int64(nm_setting_ip_tunnel_get_output_key(s_ip_tunnel),
+                                         10,
+                                         0,
+                                         G_MAXUINT32,
+                                         0);
+        lnk_vti6.fwmark = nm_setting_ip_tunnel_get_fwmark(s_ip_tunnel);
+
+        r = nm_platform_link_vti6_add(nm_device_get_platform(device), iface, &lnk_vti6, out_plink);
+        if (r < 0) {
+            g_set_error(error,
+                        NM_DEVICE_ERROR,
+                        NM_DEVICE_ERROR_CREATION_FAILED,
+                        "Failed to create VTI6 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,
@@ -986,6 +1086,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_FLAGS:
         g_value_set_uint(value, priv->flags);
         break;
+    case PROP_FWMARK:
+        g_value_set_uint(value, priv->fwmark);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -1021,7 +1124,8 @@ constructed(GObject *object)
                   NM_IP_TUNNEL_MODE_IPIP6,
                   NM_IP_TUNNEL_MODE_IP6IP6,
                   NM_IP_TUNNEL_MODE_IP6GRE,
-                  NM_IP_TUNNEL_MODE_IP6GRETAP))
+                  NM_IP_TUNNEL_MODE_IP6GRETAP,
+                  NM_IP_TUNNEL_MODE_VTI6))
         priv->addr_family = AF_INET6;
     else
         priv->addr_family = AF_INET;
@@ -1070,6 +1174,9 @@ static const NMDBusInterfaceInfoExtended interface_info_device_ip_tunnel = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FlowLabel",
                                                            "u",
                                                            NM_DEVICE_IP_TUNNEL_FLOW_LABEL),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("FwMark",
+                                                           "u",
+                                                           NM_DEVICE_IP_TUNNEL_FWMARK),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Flags",
                                                            "u",
                                                            NM_DEVICE_IP_TUNNEL_FLAGS), ), ),
@@ -1097,7 +1204,9 @@ nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass)
                                                            NM_LINK_TYPE_IP6GRE,
                                                            NM_LINK_TYPE_IP6GRETAP,
                                                            NM_LINK_TYPE_IPIP,
-                                                           NM_LINK_TYPE_SIT);
+                                                           NM_LINK_TYPE_SIT,
+                                                           NM_LINK_TYPE_VTI,
+                                                           NM_LINK_TYPE_VTI6);
 
     device_class->act_stage1_prepare          = act_stage1_prepare;
     device_class->link_changed                = link_changed;
@@ -1192,14 +1301,24 @@ nm_device_ip_tunnel_class_init(NMDeviceIPTunnelClass *klass)
                                                    0,
                                                    G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_FWMARK] = g_param_spec_uint(NM_DEVICE_IP_TUNNEL_FWMARK,
+                                                    "",
+                                                    "",
+                                                    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))
+#define NM_IP_TUNNEL_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                            \
+                                    NM_TYPE_IP_TUNNEL_DEVICE_FACTORY, \
+                                    NMIPTunnelDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
@@ -1282,7 +1401,9 @@ NM_DEVICE_FACTORY_DEFINE_INTERNAL(
                                          NM_LINK_TYPE_IPIP,
                                          NM_LINK_TYPE_IP6TNL,
                                          NM_LINK_TYPE_IP6GRE,
-                                         NM_LINK_TYPE_IP6GRETAP)
+                                         NM_LINK_TYPE_IP6GRETAP,
+                                         NM_LINK_TYPE_VTI,
+                                         NM_LINK_TYPE_VTI6)
         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;
diff --git a/src/core/devices/nm-device-ip-tunnel.h b/src/core/devices/nm-device-ip-tunnel.h
index 0a941cc2..1d7b7381 100644
--- a/src/core/devices/nm-device-ip-tunnel.h
+++ b/src/core/devices/nm-device-ip-tunnel.h
@@ -11,7 +11,7 @@
 
 #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))
+    (_NM_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))
@@ -31,6 +31,7 @@
 #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"
+#define NM_DEVICE_IP_TUNNEL_FWMARK              "fwmark"
 
 typedef struct _NMDeviceIPTunnel      NMDeviceIPTunnel;
 typedef struct _NMDeviceIPTunnelClass NMDeviceIPTunnelClass;
diff --git a/src/core/devices/nm-device-logging.h b/src/core/devices/nm-device-logging.h
index c2602189..ffc90c2d 100644
--- a/src/core/devices/nm-device-logging.h
+++ b/src/core/devices/nm-device-logging.h
@@ -11,15 +11,12 @@
 #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)))
+#define _NM_DEVICE_CAST(self) _Generic((self), NMDevice * : ((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)))
+#define _NM_DEVICE_CAST(self)                    \
+    _Generic((self), _NMLOG_DEVICE_TYPE *        \
+             : ((NMDevice *) (self)), NMDevice * \
+             : ((NMDevice *) (self)))
 #endif
 
 #undef _NMLOG_ENABLED
diff --git a/src/core/devices/nm-device-loopback.c b/src/core/devices/nm-device-loopback.c
new file mode 100644
index 00000000..ec72aa96
--- /dev/null
+++ b/src/core/devices/nm-device-loopback.c
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+#include "nm-device-loopback.h"
+#include "nm-device-private.h"
+#include "libnm-platform/nm-platform.h"
+#include "nm-device-factory.h"
+#include "nm-setting-loopback.h"
+#include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceLoopback
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+struct _NMDeviceLoopback {
+    NMDevice parent;
+};
+
+struct _NMDeviceLoopbackClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceLoopback, nm_device_loopback, NM_TYPE_DEVICE)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    /* In many aspects, loopback is a software device. Still, don't return
+     * NM_DEVICE_CAP_IS_SOFTWARE here, because we cannot delete nor create
+     * such an interface. */
+    return NM_DEVICE_CAP_NONE;
+}
+
+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_LOOPBACK,
+                                                        out_source);
+}
+
+static const char *
+get_auto_ip_config_method(NMDevice *device, int addr_family)
+{
+    return NM_IS_IPv4(addr_family) ? NM_SETTING_IP4_CONFIG_METHOD_MANUAL
+                                   : NM_SETTING_IP6_CONFIG_METHOD_MANUAL;
+}
+
+static gboolean
+complete_connection(NMDevice            *device,
+                    NMConnection        *connection,
+                    const char          *specific_object,
+                    NMConnection *const *existing_connections,
+                    GError             **error)
+{
+    nm_utils_complete_generic_with_params(nm_device_get_platform(device),
+                                          connection,
+                                          NM_SETTING_LOOPBACK_SETTING_NAME,
+                                          existing_connections,
+                                          NULL,
+                                          _("Loopback connection"),
+                                          NULL,
+                                          nm_device_get_ip_iface(device));
+
+    _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_LOOPBACK);
+
+    return TRUE;
+}
+
+static void
+update_connection(NMDevice *device, NMConnection *connection)
+{
+    _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_LOOPBACK);
+}
+
+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_LOOPBACK_SETTING_NAME)) {
+        return NM_DEVICE_CLASS(nm_device_loopback_parent_class)
+            ->can_reapply_change(device, setting_name, s_old, s_new, diffs, error);
+    }
+
+    return nm_device_hash_check_invalid_keys(diffs,
+                                             NM_SETTING_LOOPBACK_SETTING_NAME,
+                                             error,
+                                             NM_SETTING_LOOPBACK_MTU);
+}
+
+/*****************************************************************************/
+
+static const NMDBusInterfaceInfoExtended interface_info_device_loopback = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(NM_DBUS_INTERFACE_DEVICE_LOOPBACK, ),
+};
+
+static void
+nm_device_loopback_init(NMDeviceLoopback *self)
+{}
+
+static void
+nm_device_loopback_class_init(NMDeviceLoopbackClass *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_loopback);
+
+    device_class->connection_type_supported        = NM_SETTING_LOOPBACK_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_LOOPBACK_SETTING_NAME;
+    device_class->link_types = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_LOOPBACK);
+
+    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_auto_ip_config_method              = get_auto_ip_config_method;
+    device_class->get_configured_mtu                     = get_configured_mtu;
+    device_class->allow_autoconnect_on_external          = TRUE;
+    device_class->can_reapply_change                     = can_reapply_change;
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_LOOPBACK_DEVICE_FACTORY (nm_loopback_device_factory_get_type())
+#define NM_LOOPBACK_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                           \
+                                    NM_TYPE_LOOPBACK_DEVICE_FACTORY, \
+                                    NMLoopbackDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory      *factory,
+              const char           *iface,
+              const NMPlatformLink *plink,
+              NMConnection         *connection,
+              gboolean             *out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_LOOPBACK,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "Loopback",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_LOOPBACK,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_LOOPBACK,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    LOOPBACK,
+    Loopback,
+    loopback,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_LOOPBACK)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_LOOPBACK_SETTING_NAME),
+    factory_class->create_device = create_device;);
diff --git a/src/core/devices/nm-device-loopback.h b/src/core/devices/nm-device-loopback.h
new file mode 100644
index 00000000..fc62a94b
--- /dev/null
+++ b/src/core/devices/nm-device-loopback.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_LOOPBACK_H__
+#define __NETWORKMANAGER_DEVICE_LOOPBACK_H__
+
+#include "nm-device-generic.h"
+
+#define NM_TYPE_DEVICE_LOOPBACK (nm_device_loopback_get_type())
+#define NM_DEVICE_LOOPBACK(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopback))
+#define NM_DEVICE_LOOPBACK_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopbackClass))
+#define NM_IS_DEVICE_LOOPBACK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_LOOPBACK))
+#define NM_IS_DEVICE_LOOPBACK_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_LOOPBACK))
+#define NM_DEVICE_LOOPBACK_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_LOOPBACK, NMDeviceLoopbackClass))
+
+typedef struct _NMDeviceLoopback      NMDeviceLoopback;
+typedef struct _NMDeviceLoopbackClass NMDeviceLoopbackClass;
+
+GType nm_device_loopback_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_LOOPBACK_H__ */
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 5b1f5fdc..130708bb 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -45,7 +45,6 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceMacsec,
 
 typedef struct {
     NMPlatformLnkMacsec props;
-    gulong              parent_state_id;
     gulong              parent_mtu_id;
 
     struct {
@@ -92,25 +91,6 @@ static NM_UTILS_LOOKUP_STR_DEFINE(validation_mode_to_string,
                                   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.
@@ -132,27 +112,13 @@ parent_changed_notify(NMDevice *device,
     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,
+        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 */
@@ -862,7 +828,6 @@ dispose(GObject *object)
 
     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);
 }
 
@@ -1011,7 +976,7 @@ nm_device_macsec_class_init(NMDeviceMacsecClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACSEC_DEVICE_FACTORY, NMMacsecDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-macsec.h b/src/core/devices/nm-device-macsec.h
index e91fe51c..fefb773c 100644
--- a/src/core/devices/nm-device-macsec.h
+++ b/src/core/devices/nm-device-macsec.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c
index 40ecc5fd..3f57bfb1 100644
--- a/src/core/devices/nm-device-macvlan.c
+++ b/src/core/devices/nm-device-macvlan.c
@@ -29,7 +29,6 @@
 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;
@@ -103,25 +102,6 @@ platform_mode_to_string(guint mode)
 /*****************************************************************************/
 
 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.
@@ -143,27 +123,13 @@ parent_changed_notify(NMDevice *device,
     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,
+        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) {
@@ -490,7 +456,6 @@ 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
@@ -566,7 +531,7 @@ nm_device_macvlan_class_init(NMDeviceMacvlanClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_MACVLAN_DEVICE_FACTORY, NMMacvlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-macvlan.h b/src/core/devices/nm-device-macvlan.h
index 109a2bcd..86ac79c7 100644
--- a/src/core/devices/nm-device-macvlan.h
+++ b/src/core/devices/nm-device-macvlan.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c
index 6615b65e..1860df3c 100644
--- a/src/core/devices/nm-device-ppp.c
+++ b/src/core/devices/nm-device-ppp.c
@@ -337,7 +337,7 @@ nm_device_ppp_class_init(NMDevicePppClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_PPP_DEVICE_FACTORY, NMPppDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-ppp.h b/src/core/devices/nm-device-ppp.h
index 24c119ab..0e8ca54a 100644
--- a/src/core/devices/nm-device-ppp.h
+++ b/src/core/devices/nm-device-ppp.h
@@ -7,7 +7,7 @@
 #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(obj) (_NM_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))
diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c
index e623e9c0..cbea7d7d 100644
--- a/src/core/devices/nm-device-tun.c
+++ b/src/core/devices/nm-device-tun.c
@@ -527,7 +527,7 @@ nm_device_tun_class_init(NMDeviceTunClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_TUN_DEVICE_FACTORY, NMTunDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-tun.h b/src/core/devices/nm-device-tun.h
index 84497ea0..866d6c4f 100644
--- a/src/core/devices/nm-device-tun.h
+++ b/src/core/devices/nm-device-tun.h
@@ -9,7 +9,7 @@
 #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(obj) (_NM_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))
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index 170922eb..2bf24ae6 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -171,25 +171,25 @@ typedef struct {
 
 #define _NMLOG_PREFIX_NAME "resolve-addr"
 #define _NMLOG_DOMAIN      LOGD_CORE
-#define _NMLOG2(level, info, ...)                                                      \
-    G_STMT_START                                                                       \
-    {                                                                                  \
-        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                            \
-            ResolveAddrInfo *_info = (info);                                           \
-            char             _addr_str[NM_UTILS_INET_ADDRSTRLEN];                      \
-                                                                                       \
-            _nm_log((level),                                                           \
-                    (_NMLOG_DOMAIN),                                                   \
-                    0,                                                                 \
-                    NULL,                                                              \
-                    NULL,                                                              \
-                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                   \
-                                       ",%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),    \
-                    NM_HASH_OBFUSCATE_PTR(_info),                                      \
-                    nm_utils_inet_ntop(_info->addr_family, &_info->address, _addr_str) \
-                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                            \
-        }                                                                              \
-    }                                                                                  \
+#define _NMLOG2(level, info, ...)                                                   \
+    G_STMT_START                                                                    \
+    {                                                                               \
+        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                         \
+            ResolveAddrInfo *_info = (info);                                        \
+            char             _addr_str[NM_INET_ADDRSTRLEN];                         \
+                                                                                    \
+            _nm_log((level),                                                        \
+                    (_NMLOG_DOMAIN),                                                \
+                    0,                                                              \
+                    NULL,                                                           \
+                    NULL,                                                           \
+                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                \
+                                       ",%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+                    NM_HASH_OBFUSCATE_PTR(_info),                                   \
+                    nm_inet_ntop(_info->addr_family, &_info->address, _addr_str)    \
+                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                         \
+        }                                                                           \
+    }                                                                               \
     G_STMT_END
 
 static void
@@ -234,9 +234,9 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data
 static void
 resolve_addr_spawn_helper(ResolveAddrInfo *info)
 {
-    char addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char addr_str[NM_INET_ADDRSTRLEN];
 
-    nm_utils_inet_ntop(info->addr_family, &info->address, addr_str);
+    nm_inet_ntop(info->addr_family, &info->address, addr_str);
     _LOG2D(info, "start lookup via nm-daemon-helper");
     nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str),
                           g_task_get_cancellable(info->task),
diff --git a/src/core/devices/nm-device-veth.c b/src/core/devices/nm-device-veth.c
index 17115d33..c4b9e234 100644
--- a/src/core/devices/nm-device-veth.c
+++ b/src/core/devices/nm-device-veth.c
@@ -215,7 +215,7 @@ nm_device_veth_class_init(NMDeviceVethClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VETH_DEVICE_FACTORY, NMVethDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-veth.h b/src/core/devices/nm-device-veth.h
index d43a0a4b..39182363 100644
--- a/src/core/devices/nm-device-veth.h
+++ b/src/core/devices/nm-device-veth.h
@@ -9,7 +9,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c
index 0c101c70..feb011db 100644
--- a/src/core/devices/nm-device-vlan.c
+++ b/src/core/devices/nm-device-vlan.c
@@ -8,6 +8,7 @@
 #include "nm-device-vlan.h"
 
 #include <sys/socket.h>
+#include <linux/if_ether.h>
 
 #include "nm-manager.h"
 #include "nm-utils.h"
@@ -31,7 +32,6 @@
 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;
@@ -54,25 +54,6 @@ G_DEFINE_TYPE(NMDeviceVlan, nm_device_vlan, NM_TYPE_DEVICE)
 /*****************************************************************************/
 
 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.
@@ -132,19 +113,10 @@ parent_changed_notify(NMDevice *device,
     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),
@@ -156,12 +128,6 @@ parent_changed_notify(NMDevice *device,
                                                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 */
@@ -227,6 +193,8 @@ create_and_realize(NMDevice              *device,
     int                  parent_ifindex;
     guint                vlan_id;
     int                  r;
+    const char          *protocol_str;
+    guint16              protocol = ETH_P_8021Q;
 
     s_vlan = nm_connection_get_setting_vlan(connection);
     g_assert(s_vlan);
@@ -262,11 +230,22 @@ create_and_realize(NMDevice              *device,
 
     vlan_id = nm_setting_vlan_get_id(s_vlan);
 
+    protocol_str = nm_setting_vlan_get_protocol(s_vlan);
+    if (protocol_str) {
+        if (nm_streq(protocol_str, "802.1ad"))
+            protocol = ETH_P_8021AD;
+        else
+            nm_assert(nm_streq(protocol_str, "802.1Q"));
+    }
+
     r = nm_platform_link_vlan_add(nm_device_get_platform(device),
                                   iface,
                                   parent_ifindex,
-                                  vlan_id,
-                                  nm_setting_vlan_get_flags(s_vlan),
+                                  &((NMPlatformLnkVlan){
+                                      .id       = vlan_id,
+                                      .flags    = nm_setting_vlan_get_flags(s_vlan),
+                                      .protocol = protocol,
+                                  }),
                                   out_plink);
     if (r < 0) {
         g_set_error(error,
@@ -473,6 +452,9 @@ update_connection(NMDevice *device, NMConnection *connection)
         _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);
     }
+
+    if (polnk && polnk->lnk_vlan.protocol == ETH_P_8021AD)
+        g_object_set(s_vlan, NM_SETTING_VLAN_PROTOCOL, "802.1ad", NULL);
 }
 
 static NMActStageReturn
@@ -592,7 +574,7 @@ nm_device_vlan_class_init(NMDeviceVlanClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VLAN_DEVICE_FACTORY, NMVlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vlan.h b/src/core/devices/nm-device-vlan.h
index 5270706f..06fef5cf 100644
--- a/src/core/devices/nm-device-vlan.h
+++ b/src/core/devices/nm-device-vlan.h
@@ -9,7 +9,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
index 91eb195a..ad31f3a2 100644
--- a/src/core/devices/nm-device-vrf.c
+++ b/src/core/devices/nm-device-vrf.c
@@ -347,7 +347,7 @@ nm_device_vrf_class_init(NMDeviceVrfClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VRF_DEVICE_FACTORY, NMVrfDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vrf.h b/src/core/devices/nm-device-vrf.h
index 5169041c..c6580fb8 100644
--- a/src/core/devices/nm-device-vrf.h
+++ b/src/core/devices/nm-device-vrf.h
@@ -6,7 +6,7 @@
 #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(obj) (_NM_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))
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
index bf7c94c0..44a7be33 100644
--- a/src/core/devices/nm-device-vxlan.c
+++ b/src/core/devices/nm-device-vxlan.c
@@ -174,15 +174,15 @@ create_and_realize(NMDevice              *device,
 
     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))
+        if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.local)
+            && !nm_inet_parse_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))
+        if (!nm_inet_parse_bin(AF_INET, str, NULL, &props.group)
+            && !nm_inet_parse_bin(AF_INET6, str, NULL, &props.group6))
             return FALSE;
     }
 
@@ -223,7 +223,7 @@ address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6)
     if (!candidate)
         return addr4 == 0u && IN6_IS_ADDR_UNSPECIFIED(addr6);
 
-    if (!nm_utils_parse_inaddr_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
+    if (!nm_inet_parse_bin(AF_UNSPEC, candidate, &addr_family, &candidate_addr))
         return FALSE;
 
     if (!nm_ip_addr_equal(addr_family,
@@ -401,7 +401,7 @@ update_connection(NMDevice *device, NMConnection *connection)
 {
     NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device);
     NMSettingVxlan *s_vxlan    = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_VXLAN);
-    char            sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    char            sbuf[NM_INET_ADDRSTRLEN];
 
     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);
@@ -417,12 +417,12 @@ update_connection(NMDevice *device, NMConnection *connection)
         if (priv->props.group) {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_REMOTE,
-                         _nm_utils_inet4_ntop(priv->props.group, sbuf),
+                         nm_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),
+                         nm_inet6_ntop(&priv->props.group6, sbuf),
                          NULL);
         }
     }
@@ -433,12 +433,12 @@ update_connection(NMDevice *device, NMConnection *connection)
         if (priv->props.local) {
             g_object_set(s_vxlan,
                          NM_SETTING_VXLAN_LOCAL,
-                         _nm_utils_inet4_ntop(priv->props.local, sbuf),
+                         nm_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),
+                         nm_inet6_ntop(&priv->props.local6, sbuf),
                          NULL);
         }
     }
@@ -510,15 +510,15 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
         break;
     case PROP_GROUP:
         if (priv->props.group)
-            g_value_take_string(value, nm_utils_inet4_ntop_dup(priv->props.group));
+            g_value_take_string(value, nm_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));
+            g_value_take_string(value, nm_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));
+            g_value_take_string(value, nm_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));
+            g_value_take_string(value, nm_inet6_ntop_dup(&priv->props.local6));
         break;
     case PROP_TOS:
         g_value_set_uchar(value, priv->props.tos);
@@ -741,7 +741,7 @@ nm_device_vxlan_class_init(NMDeviceVxlanClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_VXLAN_DEVICE_FACTORY, NMVxlanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-vxlan.h b/src/core/devices/nm-device-vxlan.h
index 4c4165e5..e53ae82e 100644
--- a/src/core/devices/nm-device-vxlan.h
+++ b/src/core/devices/nm-device-vxlan.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index 179c2a22..db02033d 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -219,7 +219,7 @@ _auto_default_route_get_enabled(NMSettingWireGuard *s_wg,
                 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))
+                if (!nm_inet_parse_with_prefix_bin(AF_UNSPEC, aip, &addr_family, NULL, &prefix))
                     continue;
                 if (prefix != 0)
                     continue;
@@ -1152,11 +1152,11 @@ _peers_get_platform_list(NMDeviceWireGuardPrivate            *priv,
 
                 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)) {
+                    || !nm_inet_parse_with_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;
@@ -1197,7 +1197,7 @@ skip:
             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);
+                &nm_g_array_index(allowed_ips, NMPWireGuardAllowedIP, plp->_construct_idx_start);
             plp->allowed_ips_len = l;
         }
     }
@@ -1701,8 +1701,7 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
 
             aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
 
-            if (!valid
-                || !nm_utils_parse_inaddr_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
+            if (!valid || !nm_inet_parse_with_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
                 continue;
 
             if (prefix < 0)
@@ -1722,7 +1721,7 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
                                             NM_L3_CONFIG_DAT_FLAGS_IGNORE_MERGE_NO_DEFAULT_ROUTES);
             }
 
-            nm_utils_ipx_address_clear_host_address(addr_family, &addrbin, NULL, prefix);
+            nm_ip_addr_clear_host_address(addr_family, &addrbin, NULL, prefix);
 
             rtable_coerced = route_table_coerced;
 
@@ -2062,8 +2061,10 @@ nm_device_wireguard_class_init(NMDeviceWireGuardClass *klass)
 /*************************************************************/
 
 #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))
+#define NM_WIREGUARD_DEVICE_FACTORY(obj)                              \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj),                            \
+                                    NM_TYPE_WIREGUARD_DEVICE_FACTORY, \
+                                    NMWireGuardDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-wireguard.h b/src/core/devices/nm-device-wireguard.h
index 7e18bdba..8fb8f8cf 100644
--- a/src/core/devices/nm-device-wireguard.h
+++ b/src/core/devices/nm-device-wireguard.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c
index ecb5ee23..98356ccf 100644
--- a/src/core/devices/nm-device-wpan.c
+++ b/src/core/devices/nm-device-wpan.c
@@ -221,7 +221,7 @@ nm_device_wpan_class_init(NMDeviceWpanClass *klass)
 
 #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))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_WPAN_DEVICE_FACTORY, NMWpanDeviceFactory))
 
 static NMDevice *
 create_device(NMDeviceFactory      *factory,
diff --git a/src/core/devices/nm-device-wpan.h b/src/core/devices/nm-device-wpan.h
index 969929ae..39cf4656 100644
--- a/src/core/devices/nm-device-wpan.h
+++ b/src/core/devices/nm-device-wpan.h
@@ -7,7 +7,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index c4f0f9a2..04478614 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -18,7 +18,7 @@
 #include <netinet/in.h>
 #include <netinet/if_ether.h>
 #include <linux/if.h>
-#include <linux/if_addr.h>
+#include "nm-compat-headers/linux/if_addr.h"
 #include <linux/rtnetlink.h>
 #include <linux/if_ether.h>
 #include <linux/if_infiniband.h>
@@ -59,6 +59,7 @@
 #include "settings/nm-settings.h"
 #include "nm-setting-ethtool.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "nm-setting-user.h"
 #include "nm-auth-utils.h"
 #include "nm-keep-alive.h"
@@ -77,6 +78,7 @@
 
 #include "nm-device-generic.h"
 #include "nm-device-bridge.h"
+#include "nm-device-loopback.h"
 #include "nm-device-vlan.h"
 #include "nm-device-vrf.h"
 #include "nm-device-wireguard.h"
@@ -110,6 +112,7 @@ typedef enum {
     CLEANUP_TYPE_KEEP,
     CLEANUP_TYPE_REMOVED,
     CLEANUP_TYPE_DECONFIGURE,
+    CLEANUP_TYPE_KEEP_REAPPLY,
 } CleanupType;
 
 typedef enum _nm_packed {
@@ -848,7 +851,7 @@ static void _dev_ipshared4_spawn_dnsmasq(NMDevice *self);
 static void _dev_ipshared6_start(NMDevice *self);
 
 static void
-_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean from_reapply);
+_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean preserve_dhcp);
 
 static void concheck_update_state(NMDevice           *self,
                                   int                 addr_family,
@@ -1583,6 +1586,9 @@ _prop_get_ipv4_link_local(NMDevice *self)
     if (!s_ip4)
         return NM_SETTING_IP4_LL_DISABLED;
 
+    if (NM_IS_DEVICE_LOOPBACK(self))
+        return NM_SETTING_IP4_LL_DISABLED;
+
     link_local = nm_setting_ip4_config_get_link_local(s_ip4);
 
     if (link_local == NM_SETTING_IP4_LL_DEFAULT) {
@@ -2736,12 +2742,6 @@ _ethtool_state_set(NMDevice *self)
 
 /*****************************************************************************/
 
-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)
 {
@@ -3578,7 +3578,7 @@ _dev_ip_state_check_async(NMDevice *self, int addr_family)
 }
 
 static void
-_dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
+_dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean keep_reapply)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
     int              IS_IPv4;
@@ -3586,7 +3586,7 @@ _dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
     if (addr_family == AF_UNSPEC) {
         _dev_ip_state_set_state(self,
                                 addr_family,
-                                from_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
+                                keep_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
                                 "ip-state-clear");
         return;
     }
@@ -3597,7 +3597,7 @@ _dev_ip_state_cleanup(NMDevice *self, int addr_family, gboolean from_reapply)
     nm_clear_g_source_inst(&priv->ip_data_x[IS_IPv4].req_timeout_source);
     _dev_ip_state_set_state(self,
                             addr_family,
-                            from_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
+                            keep_reapply ? NM_DEVICE_IP_STATE_PENDING : NM_DEVICE_IP_STATE_NONE,
                             "ip-state-clear");
     priv->ip_data_x[IS_IPv4].wait_for_carrier = FALSE;
     priv->ip_data_x[IS_IPv4].wait_for_ports   = FALSE;
@@ -4129,18 +4129,30 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         /* Check if AC6 addresses completed DAD */
         if (NM_FLAGS_ANY(notify_data->platform_change_on_idle.obj_type_flags,
                          nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP6_ADDRESS))
-            && priv->ipac6_data.state == NM_DEVICE_IP_STATE_PENDING && priv->ipac6_data.l3cd
-            && nm_l3cfg_check_ready(l3cfg,
-                                    priv->ipac6_data.l3cd,
-                                    AF_INET6,
-                                    NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
-                                    NULL)) {
-            if (nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, AF_INET6))
-                _dev_l3_cfg_commit(self, FALSE);
-
-            nm_clear_l3cd(&priv->ipac6_data.l3cd);
-            _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
-            _dev_ip_state_check_async(self, AF_INET6);
+            && priv->ipac6_data.state == NM_DEVICE_IP_STATE_PENDING && priv->ipac6_data.l3cd) {
+            gs_unref_array GArray *conflicts = NULL;
+            gboolean               ready;
+
+            ready = nm_l3cfg_check_ready(l3cfg,
+                                         priv->ipac6_data.l3cd,
+                                         AF_INET6,
+                                         NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
+                                         &conflicts);
+            if (conflicts) {
+                /* nm_ndisc_dad_failed() will emit a new "NDisc:config-received"
+                 * signal; _dev_ipac6_ndisc_config_changed() will be called
+                 * synchronously to update the current state and schedule a commit. */
+                nm_ndisc_dad_failed(priv->ipac6_data.ndisc, conflicts, TRUE);
+            } else if (ready) {
+                if (nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, AF_INET6))
+                    _dev_l3_cfg_commit(self, FALSE);
+
+                nm_clear_l3cd(&priv->ipac6_data.l3cd);
+                _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
+                _dev_ip_state_check_async(self, AF_INET6);
+            } else {
+                /* wait */
+            }
         }
 
         _dev_ipmanual_check_ready(self);
@@ -5005,6 +5017,9 @@ nm_device_get_route_metric_default(NMDeviceType device_type)
      */
 
     switch (device_type) {
+    case NM_DEVICE_TYPE_LOOPBACK:
+        return 30;
+
     /* 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
@@ -5425,7 +5440,7 @@ concheck_is_possible(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    if (!nm_device_is_real(self) || is_loopback(self))
+    if (!nm_device_is_real(self) || NM_IS_DEVICE_LOOPBACK(self))
         return FALSE;
 
     /* we enable periodic checks for every device state (except UNKNOWN). Especially with
@@ -6203,7 +6218,8 @@ nm_device_master_release_slave(NMDevice           *self,
     NMDevicePrivate          *priv;
     NMDevicePrivate          *slave_priv;
     SlaveInfo                *info;
-    gs_unref_object NMDevice *self_free = NULL;
+    gs_unref_object NMDevice *self_free  = NULL;
+    gs_unref_object NMDevice *slave_free = NULL;
 
     g_return_if_fail(NM_DEVICE(self));
     g_return_if_fail(NM_DEVICE(slave));
@@ -6246,14 +6262,15 @@ nm_device_master_release_slave(NMDevice           *self,
 
     /* keep both alive until the end of the function.
      * Transfers ownership from slave_priv->master.  */
-    self_free = self;
+    nm_assert(self == slave_priv->master);
+    self_free = g_steal_pointer(&slave_priv->master);
 
-    c_list_unlink(&info->lst_slave);
-    slave_priv->master = NULL;
+    nm_assert(slave == info->slave);
+    slave_free = g_steal_pointer(&info->slave);
 
+    c_list_unlink(&info->lst_slave);
     g_signal_handler_disconnect(slave, info->watch_id);
-    g_object_unref(slave);
-    g_slice_free(SlaveInfo, info);
+    nm_g_slice_free(info);
 
     if (c_list_is_empty(&priv->slaves)) {
         _active_connection_set_state_flags_full(self,
@@ -6577,27 +6594,30 @@ device_recheck_slave_status(NMDevice *self, const NMPlatformLink *plink)
                                        NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
     }
 
-    if (master && NM_DEVICE_GET_CLASS(master)->attach_port) {
-        nm_device_master_add_slave(master, self, FALSE);
+    if (master) {
+        if (NM_DEVICE_GET_CLASS(master)->attach_port) {
+            nm_device_master_add_slave(master, self, FALSE);
+        } else {
+            _LOGD(LOGD_DEVICE,
+                  "enslaved to non-master-type device %s; ignoring",
+                  nm_device_get_iface(master));
+        }
         goto out;
     }
 
-    if (master) {
-        _LOGD(LOGD_DEVICE,
-              "enslaved to non-master-type device %s; ignoring",
-              nm_device_get_iface(master));
-    } else {
+    if (plink->master) {
         _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);
+        }
     }
-    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:
@@ -6609,6 +6629,8 @@ device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
+    g_return_if_fail(priv->master_ifindex > 0);
+
     if (priv->master_ifindex != nm_device_get_ifindex(device_changed))
         return;
 
@@ -7465,11 +7487,6 @@ realize_start_setup(NMDevice             *self,
                                   NM_UNMANAGED_EXTERNAL_DOWN,
                                   _dev_unmanaged_is_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);
 
@@ -7659,8 +7676,7 @@ nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error)
     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_USER_UDEV | NM_UNMANAGED_USER_EXPLICIT
                                       | NM_UNMANAGED_EXTERNAL_DOWN | NM_UNMANAGED_IS_SLAVE,
                                   NM_UNMAN_FLAG_OP_FORGET);
 
@@ -8585,7 +8601,7 @@ nm_device_generate_connection(NMDevice *self,
 
         pllink = nm_platform_link_get(nm_device_get_platform(self), priv->ifindex);
         if (pllink && pllink->inet6_token.id) {
-            char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+            char sbuf[NM_INET_ADDRSTRLEN];
 
             g_object_set(s_ip6,
                          NM_SETTING_IP6_CONFIG_ADDR_GEN_MODE,
@@ -8776,6 +8792,14 @@ check_connection_compatible(NMDevice *self, NMConnection *connection, GError **e
         return FALSE;
     }
 
+    if (!nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)
+        && nm_connection_get_setting(connection, NM_TYPE_SETTING_SRIOV)) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "device does not support SR-IOV");
+        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
@@ -9359,14 +9383,17 @@ activate_stage1_device_prepare(NMDevice *self)
     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;
+        NMSettingSriov *s_sriov = NULL;
 
         if (nm_device_sys_iface_state_is_external_or_assume(self)) {
             /* pass */
-        } else if (priv->stage1_sriov_state == NM_DEVICE_STAGE_STATE_PENDING)
+        } 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))) {
+        } else if (priv->ifindex > 0) {
+            s_sriov = nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV);
+        }
+
+        if (s_sriov) {
             nm_auto_freev NMPlatformVF **plat_vfs = NULL;
             gs_free_error GError        *error    = NULL;
             NMSriovVF                   *vf;
@@ -9374,6 +9401,8 @@ activate_stage1_device_prepare(NMDevice *self)
             guint                        num;
             guint                        i;
 
+            nm_assert(nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV));
+
             autoprobe = nm_setting_sriov_get_autoprobe_drivers(s_sriov);
             if (autoprobe == NM_TERNARY_DEFAULT) {
                 autoprobe = nm_config_data_get_connection_default_int64(
@@ -10058,7 +10087,7 @@ _dev_ipmanual_check_ready(NMDevice *self)
     gboolean               has_carrier;
     NML3CfgCheckReadyFlags flags;
     gboolean               ready;
-    gboolean               acd_used = FALSE;
+    gs_unref_array GArray *conflicts = NULL;
     int                    IS_IPv4;
 
     if (priv->ipmanual_data.state_4 != NM_DEVICE_IP_STATE_PENDING
@@ -10101,8 +10130,8 @@ _dev_ipmanual_check_ready(NMDevice *self)
                                      priv->l3cds[L3_CONFIG_DATA_TYPE_MANUALIP].d,
                                      addr_family,
                                      flags,
-                                     &acd_used);
-        if (acd_used) {
+                                     &conflicts);
+        if (conflicts) {
             _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_FAILED);
             _dev_ip_state_check_async(self, AF_UNSPEC);
         } else if (ready) {
@@ -10434,6 +10463,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                     .request_broadcast = request_broadcast,
                     .acd_timeout_msec  = _prop_get_ipv4_dad_timeout(self),
                 },
+            .previous_lease = priv->l3cds[L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4)].d,
         };
 
         priv->ipdhcp_data_4.client =
@@ -10484,16 +10514,16 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                          G_CALLBACK(_dev_ipdhcpx_notify),
                          self);
 
-    /* FIXME(l3cfg:dhcp:previous-lease): take the NML3ConfigData from the previous lease (if any)
-     * and pass it on to NMDhcpClient. This is a fake lease that we use initially (until
-     * NMDhcpClient got a real lease). Note that NMDhcpClient needs to check whether the
-     * lease already expired. */
-
+    /* Take the NML3ConfigData from the previous lease (if any) that was passed to the NMDhcpClient.
+     * This may be the old lease only used during the duration of a reapply until we get the
+     * new lease. */
     previous_lease = nm_dhcp_client_get_lease(priv->ipdhcp_data_x[IS_IPv4].client);
+
     if (!priv->ipdhcp_data_x[IS_IPv4].config) {
         priv->ipdhcp_data_x[IS_IPv4].config = nm_dhcp_config_new(addr_family, previous_lease);
         _notify(self, PROP_DHCPX_CONFIG(IS_IPv4));
     }
+
     if (previous_lease) {
         nm_dhcp_config_set_lease(priv->ipdhcp_data_x[IS_IPv4].config, previous_lease);
         _dev_l3_register_l3cds_set_one_full(self,
@@ -10747,14 +10777,14 @@ nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device)
         l3cd_src = priv_src->l3cds[L3_CONFIG_DATA_TYPE_AC_6].d;
     }
     if (l3cd_src) {
-        const char *const     *strvarr;
-        const struct in6_addr *addrs;
-        guint                  n;
-        guint                  i;
+        const char *const *strvarr;
+        const char *const *addrs;
+        guint              n;
+        guint              i;
 
         addrs = nm_l3_config_data_get_nameservers(l3cd_src, AF_INET6, &n);
         for (i = 0; i < n; i++)
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &addrs[i]);
+            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, addrs[i]);
 
         strvarr = nm_l3_config_data_get_searches(l3cd_src, AF_INET6, &n);
         for (i = 0; i < n; i++)
@@ -10806,7 +10836,8 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
               || (!priv->ipll_data_6.v6.ipv6ll
                   && NM_IN_SET(priv->ipll_data_6.v6.llstate,
                                NM_L3_IPV6LL_STATE_NONE,
-                               NM_L3_IPV6LL_STATE_DEFUNCT)));
+                               NM_L3_IPV6LL_STATE_DEFUNCT,
+                               NM_L3_IPV6LL_STATE_READY)));
 
     switch (priv->ipll_data_6.v6.llstate) {
     case NM_L3_IPV6LL_STATE_NONE:
@@ -10844,7 +10875,7 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
     }
 
     if (changed) {
-        char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+        char sbuf[NM_INET_ADDRSTRLEN];
 
         _LOGT_ipll(AF_INET6,
                    "set state %s (was %s, llstate=%s, lladdr=%s)",
@@ -10853,7 +10884,7 @@ _dev_ipll6_set_llstate(NMDevice *self, NML3IPv6LLState llstate, const struct in6
                    nm_l3_ipv6ll_state_to_string(priv->ipll_data_6.v6.llstate),
                    nm_ip_addr_is_null(AF_INET6, &priv->ipll_data_6.v6.lladdr)
                        ? "(none)"
-                       : _nm_utils_inet6_ntop(&priv->ipll_data_6.v6.lladdr, sbuf));
+                       : nm_inet6_ntop(&priv->ipll_data_6.v6.lladdr, sbuf));
     }
 
     if (changed)
@@ -10889,6 +10920,11 @@ _dev_ipll6_start(NMDevice *self)
     if (priv->ipll_data_6.v6.ipv6ll)
         return;
 
+    if (NM_IS_DEVICE_LOOPBACK(self)) {
+        _dev_ipll6_set_llstate(self, NM_L3_IPV6LL_STATE_READY, NULL);
+        return;
+    }
+
     if (!priv->l3cfg) {
         _LOGD(LOGD_IP6, "linklocal6: no IP link for IPv6");
         goto out_fail;
@@ -11000,6 +11036,10 @@ nm_device_get_configured_mtu_from_connection(NMDevice          *self,
         if (setting)
             mtu = nm_setting_wireguard_get_mtu(NM_SETTING_WIREGUARD(setting));
         global_property_name = NM_CON_DEFAULT("wireguard.mtu");
+    } else if (setting_type == NM_TYPE_SETTING_LOOPBACK) {
+        if (setting)
+            mtu = nm_setting_loopback_get_mtu(NM_SETTING_LOOPBACK(setting));
+        global_property_name = NM_CON_DEFAULT("loopback.mtu");
     } else
         g_return_val_if_reached(0);
 
@@ -11411,10 +11451,14 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                 const NML3ConfigData *l3cd,
                                 NMDevice             *self)
 {
-    NMDevicePrivate *priv  = NM_DEVICE_GET_PRIVATE(self);
-    gboolean         ready = TRUE;
-    NMDedupMultiIter iter;
-    const NMPObject *obj;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         ready;
+
+    /* The ndisc configuration changes when we receive a new RA or
+     * when a lifetime expires; but also when DAD fails for a
+     * SLAAC address and we need to regenerate new stable-privacy
+     * addresses. In all these cases we update the AC6 configuration,
+     * schedule a commit and update the AC state. */
 
     _dev_ipac6_grace_period_start(self, 0, TRUE);
 
@@ -11425,22 +11469,11 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                         FALSE);
 
     nm_clear_l3cd(&priv->ipac6_data.l3cd);
-
-    /* wait that addresses are committed to platform and
-     * become non-tentative before declaring AC6 is ready.*/
-    nm_l3_config_data_iter_obj_for_each (&iter, l3cd, &obj, NMP_OBJECT_TYPE_IP6_ADDRESS) {
-        const NMPlatformIP6Address *addr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
-        const NMPlatformIP6Address *plat_addr;
-
-        plat_addr = nm_platform_ip6_address_get(nm_device_get_platform(self),
-                                                nm_device_get_ip_ifindex(self),
-                                                &addr->address);
-        if (!plat_addr || (plat_addr->n_ifa_flags & IFA_F_TENTATIVE)) {
-            ready = FALSE;
-            break;
-        }
-    }
-
+    ready = nm_l3cfg_check_ready(priv->l3cfg,
+                                 l3cd,
+                                 AF_INET6,
+                                 NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
+                                 NULL);
     if (ready) {
         _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
     } else {
@@ -12078,13 +12111,34 @@ activate_stage3_ip_config(NMDevice *self)
 
     ifindex = nm_device_get_ip_ifindex(self);
 
+    ipv4_method = nm_device_get_effective_ip_config_method(self, AF_INET);
+    if (nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+        /* "auto" usually means DHCPv4 or autoconf6, but it doesn't have to be. Subclasses
+         * can overwrite it. For example, you cannot run DHCPv4 on PPP/WireGuard links. */
+        ipv4_method = klass->get_ip_method_auto(self, AF_INET);
+    }
+
+    ipv6_method = nm_device_get_effective_ip_config_method(self, AF_INET6);
+
+    if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+        ipv6_method = klass->get_ip_method_auto(self, AF_INET6);
+    }
+
     if (priv->ip_data_4.do_reapply) {
         _LOGD_ip(AF_INET, "reapply...");
-        _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_DECONFIGURE, TRUE);
+        priv->ip_data_4.do_reapply = FALSE;
+        _cleanup_ip_pre(self,
+                        AF_INET,
+                        CLEANUP_TYPE_KEEP_REAPPLY,
+                        nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO));
     }
     if (priv->ip_data_6.do_reapply) {
         _LOGD_ip(AF_INET6, "reapply...");
-        _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_DECONFIGURE, TRUE);
+        priv->ip_data_6.do_reapply = FALSE;
+        _cleanup_ip_pre(self,
+                        AF_INET6,
+                        CLEANUP_TYPE_KEEP_REAPPLY,
+                        nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO));
     }
 
     /* Add the interface to the specified firewall zone */
@@ -12136,18 +12190,6 @@ activate_stage3_ip_config(NMDevice *self)
      * let's do it! */
     _commit_mtu(self);
 
-    ipv4_method = nm_device_get_effective_ip_config_method(self, AF_INET);
-    if (nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
-        /* "auto" usually means DHCPv4 or autoconf6, but it doesn't have to be. Subclasses
-         * can overwrite it. For example, you cannot run DHCPv4 on PPP/WireGuard links. */
-        ipv4_method = klass->get_ip_method_auto(self, AF_INET);
-    }
-
-    ipv6_method = nm_device_get_effective_ip_config_method(self, AF_INET6);
-    if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
-        ipv6_method = klass->get_ip_method_auto(self, AF_INET6);
-    }
-
     if (!nm_device_sys_iface_state_is_external(self)
         && (!klass->ready_for_ip_config || klass->ready_for_ip_config(self, TRUE))) {
         if (priv->ipmanual_data.state_6 == NM_DEVICE_IP_STATE_NONE
@@ -12274,16 +12316,28 @@ _dev_ipshared4_new_l3cd(NMDevice *self, NMConnection *connection, NMPlatformIP4A
 static gboolean
 _dev_ipshared4_init(NMDevice *self)
 {
-    static 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"};
-    int                      errsv;
-    guint                    i;
+    static const char *const modules_iptables[] = {"ip_tables", "iptable_nat"};
+    static const char *const modules_nftables[] =
+        {"nf_nat_ftp", "nf_nat_irc", "nf_nat_sip", "nf_nat_tftp", "nf_nat_pptp", "nf_nat_h323"};
+    int   errsv;
+    guint i;
+
+    switch (nm_firewall_utils_get_backend()) {
+    case NM_FIREWALL_BACKEND_IPTABLES:
+        for (i = 0; i < G_N_ELEMENTS(modules_iptables); i++)
+            nmp_utils_modprobe(NULL, FALSE, modules_iptables[i], NULL);
+        break;
+    case NM_FIREWALL_BACKEND_NFTABLES:
+        for (i = 0; i < G_N_ELEMENTS(modules_nftables); i++)
+            nmp_utils_modprobe(NULL, FALSE, modules_nftables[i], NULL);
+        break;
+    case NM_FIREWALL_BACKEND_NONE:
+        /* do not modify network settings like ip forwarding */
+        return TRUE;
+    default:
+        nm_assert_not_reached();
+        break;
+    }
 
     if (nm_platform_sysctl_get_int32(nm_device_get_platform(self),
                                      NMP_SYSCTL_PATHID_ABSOLUTE("/proc/sys/net/ipv4/ip_forward"),
@@ -12312,9 +12366,6 @@ _dev_ipshared4_init(NMDevice *self)
                        nm_strerror_native(errsv));
     }
 
-    for (i = 0; i < G_N_ELEMENTS(modules); i++)
-        nmp_utils_modprobe(NULL, FALSE, modules[i], NULL);
-
     return TRUE;
 }
 
@@ -12591,17 +12642,18 @@ delete_on_deactivate_check_and_schedule(NMDevice *self)
 }
 
 static void
-_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean from_reapply)
+_cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gboolean preserve_dhcp)
 {
-    const int        IS_IPv4 = NM_IS_IPv4(addr_family);
-    NMDevicePrivate *priv    = NM_DEVICE_GET_PRIVATE(self);
+    const int        IS_IPv4      = NM_IS_IPv4(addr_family);
+    NMDevicePrivate *priv         = NM_DEVICE_GET_PRIVATE(self);
+    gboolean         keep_reapply = (cleanup_type == CLEANUP_TYPE_KEEP_REAPPLY);
 
     _dev_ipsharedx_cleanup(self, addr_family);
 
     _dev_ipdev_cleanup(self, AF_UNSPEC);
     _dev_ipdev_cleanup(self, addr_family);
 
-    _dev_ipdhcpx_cleanup(self, addr_family, TRUE, FALSE);
+    _dev_ipdhcpx_cleanup(self, addr_family, !preserve_dhcp || !keep_reapply, FALSE);
 
     if (!IS_IPv4)
         _dev_ipac6_cleanup(self);
@@ -12613,8 +12665,8 @@ _cleanup_ip_pre(NMDevice *self, int addr_family, CleanupType cleanup_type, gbool
     nm_clear_g_signal_handler(nm_manager_get_dns_manager(priv->manager),
                               &priv->ip_data.dnsmgr_update_pending_signal_id);
 
-    _dev_ip_state_cleanup(self, AF_UNSPEC, from_reapply);
-    _dev_ip_state_cleanup(self, addr_family, from_reapply);
+    _dev_ip_state_cleanup(self, AF_UNSPEC, keep_reapply);
+    _dev_ip_state_cleanup(self, addr_family, keep_reapply);
 }
 
 gboolean
@@ -12762,7 +12814,9 @@ can_reapply_change(NMDevice   *self,
         goto out_fail;
     }
 
-    if (nm_streq(setting_name, NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME)
+    if (NM_IN_STRSET(setting_name,
+                     NM_SETTING_OVS_EXTERNAL_IDS_SETTING_NAME,
+                     NM_SETTING_OVS_OTHER_CONFIG_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. */
@@ -12984,6 +13038,10 @@ check_and_reapply_connection(NMDevice            *self,
     if (priv->state >= NM_DEVICE_STATE_ACTIVATED)
         nm_device_update_metered(self);
 
+    /* Notify dispatcher when re-applied */
+    _LOGD(LOGD_DEVICE, "Notifying re-apply complete");
+    nm_dispatcher_call_device(NM_DISPATCHER_ACTION_REAPPLY, self, NULL, NULL, NULL, NULL);
+
     return TRUE;
 }
 
@@ -13889,7 +13947,7 @@ nm_device_start_ip_check(NMDevice *self)
     NMSettingConnection *s_con;
     guint                timeout     = 0;
     const char          *ping_binary = NULL;
-    char                 buf[NM_UTILS_INET_ADDRSTRLEN];
+    char                 buf[NM_INET_ADDRSTRLEN];
     NMLogDomain          log_domain = LOGD_IP4;
 
     /* Shouldn't be any active ping here, since IP_CHECK happens after the
@@ -13920,14 +13978,14 @@ nm_device_start_ip_check(NMDevice *self)
         } else if (priv->ip_data_4.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET);
             if (gw) {
-                _nm_utils_inet4_ntop(NMP_OBJECT_CAST_IP4_ROUTE(gw)->gateway, buf);
+                nm_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_data_6.state == NM_DEVICE_IP_STATE_READY) {
             gw = nm_l3_config_data_get_best_default_route(l3cd, AF_INET6);
             if (gw) {
-                _nm_utils_inet6_ntop(&NMP_OBJECT_CAST_IP6_ROUTE(gw)->gateway, buf);
+                nm_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;
             }
@@ -14151,8 +14209,6 @@ 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"),
@@ -14847,9 +14903,10 @@ _nm_device_check_connection_available(NMDevice                      *self,
         } 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");
+                nm_utils_error_set_literal(
+                    error,
+                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_STRICTLY_UNMANAGED_DEVICE,
+                    "device is strictly unmanaged");
                 return FALSE;
             }
             if (!NM_FLAGS_HAS(flags,
@@ -16138,7 +16195,7 @@ nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason r
 
     /* We should only ever have one delayed state transition at a time */
     if (priv->queued_state.id) {
-        _LOGW(LOGD_DEVICE,
+        _LOGD(LOGD_DEVICE,
               "queue-state[%s, reason:%s, id:%u]: %s",
               nm_device_state_to_string(priv->queued_state.state),
               nm_device_state_reason_to_string_a(priv->queued_state.reason),
@@ -17127,6 +17184,12 @@ nm_device_clear_dns_lookup_data(NMDevice *self)
         nm_clear_pointer(&priv->hostname_resolver_x[i], _hostname_resolver_free);
 }
 
+gboolean
+nm_device_get_allow_autoconnect_on_external(NMDevice *self)
+{
+    return NM_DEVICE_GET_CLASS(self)->allow_autoconnect_on_external;
+}
+
 static GInetAddress *
 get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
 {
@@ -17151,10 +17214,12 @@ get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
 
     if (head_entry) {
         c_list_for_each_entry (iter, &head_entry->lst_entries_head, lst_entries) {
-            const NMPlatformIPAddress *addr = NMP_OBJECT_CAST_IP_ADDRESS(iter->obj);
+            const NMPlatformIPXAddress *addr = NMP_OBJECT_CAST_IPX_ADDRESS(iter->obj);
 
             if (IS_IPv4) {
-                return g_inet_address_new_from_bytes(addr->address_ptr, G_SOCKET_FAMILY_IPV4);
+                if (nm_ip4_addr_is_loopback(addr->a4.address))
+                    continue;
+                return g_inet_address_new_from_bytes(addr->ax.address_ptr, G_SOCKET_FAMILY_IPV4);
             }
 
             /* For IPv6 prefer, in order:
@@ -17163,15 +17228,19 @@ get_address_for_hostname_dns_lookup(NMDevice *self, int addr_family)
              * - link-local
              */
 
-            if (!IN6_IS_ADDR_LINKLOCAL(addr->address_ptr)) {
-                if (!(addr->n_ifa_flags & IFA_F_DEPRECATED)) {
-                    return g_inet_address_new_from_bytes(addr->address_ptr, G_SOCKET_FAMILY_IPV6);
+            if (IN6_ARE_ADDR_EQUAL(&addr->a6.address, &in6addr_loopback))
+                continue;
+
+            if (!IN6_IS_ADDR_LINKLOCAL(addr->ax.address_ptr)) {
+                if (!(addr->ax.n_ifa_flags & IFA_F_DEPRECATED)) {
+                    return g_inet_address_new_from_bytes(addr->ax.address_ptr,
+                                                         G_SOCKET_FAMILY_IPV6);
                 }
-                addr6_nonll = addr->address_ptr;
+                addr6_nonll = addr->ax.address_ptr;
                 continue;
             }
 
-            addr6_ll = addr->address_ptr;
+            addr6_ll = addr->ax.address_ptr;
         }
 
         if (addr6_nonll || addr6_ll)
@@ -17578,7 +17647,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN);
         priv->type = g_value_get_uint(value);
         nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN);
-        nm_assert(priv->type <= NM_DEVICE_TYPE_VRF);
+        nm_assert(priv->type <= NM_DEVICE_TYPE_LOOPBACK);
         break;
     case PROP_LINK_TYPE:
         /* construct-only */
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index de850e68..f54457d1 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -91,7 +91,7 @@
 #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(obj)            (_NM_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))
@@ -208,6 +208,8 @@ typedef struct _NMDeviceClass {
 
     bool can_reapply_change_ovs_external_ids : 1;
 
+    bool allow_autoconnect_on_external : 1;
+
     NMRfkillType rfkill_type : 4;
 
     void (*state_changed)(NMDevice           *device,
@@ -561,8 +563,6 @@ void nm_device_copy_ip6_dns_config(NMDevice *self, NMDevice *from_device);
  * @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
@@ -592,21 +592,19 @@ typedef enum {
      * 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),
+    NM_UNMANAGED_PLATFORM_INIT = (1LL << 2),
+    NM_UNMANAGED_USER_EXPLICIT = (1LL << 3),
+    NM_UNMANAGED_USER_SETTINGS = (1LL << 4),
 
     /* These flags can be non-effective and be overwritten
      * by other flags. */
-    NM_UNMANAGED_BY_DEFAULT    = (1LL << 7),
-    NM_UNMANAGED_USER_CONF     = (1LL << 8),
-    NM_UNMANAGED_USER_UDEV     = (1LL << 9),
-    NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 10),
-    NM_UNMANAGED_IS_SLAVE      = (1LL << 11),
+    NM_UNMANAGED_BY_DEFAULT    = (1LL << 5),
+    NM_UNMANAGED_USER_CONF     = (1LL << 6),
+    NM_UNMANAGED_USER_UDEV     = (1LL << 7),
+    NM_UNMANAGED_EXTERNAL_DOWN = (1LL << 8),
+    NM_UNMANAGED_IS_SLAVE      = (1LL << 9),
 
-    NM_UNMANAGED_ALL = ((1LL << 12) - 1),
+    NM_UNMANAGED_ALL = ((1LL << 10) - 1),
 } NMUnmanagedFlags;
 
 typedef enum {
@@ -821,4 +819,6 @@ nm_device_get_hostname_from_dns_lookup(NMDevice *self, int addr_family, gboolean
 
 void nm_device_clear_dns_lookup_data(NMDevice *self);
 
+gboolean nm_device_get_allow_autoconnect_on_external(NMDevice *self);
+
 #endif /* __NETWORKMANAGER_DEVICE_H__ */
diff --git a/src/core/devices/nm-lldp-listener.c b/src/core/devices/nm-lldp-listener.c
index 137bf8fb..ac7e97f0 100644
--- a/src/core/devices/nm-lldp-listener.c
+++ b/src/core/devices/nm-lldp-listener.c
@@ -15,24 +15,20 @@
 #include "libnm-std-aux/unaligned.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-glib-aux/nm-c-list.h"
+#include "libnm-lldp/nm-lldp-rx.h"
 #include "nm-utils.h"
 
-#include "libnm-systemd-core/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}}))
+#define LLDP_MAC_NEAREST_BRIDGE          (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e))
+#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x03))
+#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE (&NM_ETHER_ADDR_INIT(0x01, 0x80, 0xc2, 0x00, 0x00, 0x00))
 
 /*****************************************************************************/
 
 struct _NMLldpListener {
-    sd_lldp_rx *lldp_handle;
+    NMLldpRX   *lldp_rx;
     GHashTable *lldp_neighbors;
     GVariant   *variant;
 
@@ -49,12 +45,12 @@ struct _NMLldpListener {
 /*****************************************************************************/
 
 typedef struct {
-    GVariant         *variant;
-    sd_lldp_neighbor *neighbor_sd;
-    char             *chassis_id;
-    char             *port_id;
-    guint8            chassis_id_type;
-    guint8            port_id_type;
+    GVariant       *variant;
+    NMLldpNeighbor *neighbor_nm;
+    char           *chassis_id;
+    char           *port_id;
+    guint8          chassis_id_type;
+    guint8          port_id_type;
 } LldpNeighbor;
 
 /*****************************************************************************/
@@ -100,7 +96,7 @@ lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *o
 
     nm_assert(neigh);
 
-    r = sd_lldp_neighbor_get_raw(neigh->neighbor_sd, &raw_data, &raw_len);
+    r = nm_lldp_neighbor_get_raw(neigh->neighbor_nm, &raw_data, &raw_len);
 
     nm_assert(r >= 0);
     nm_assert(raw_data);
@@ -111,24 +107,24 @@ lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *o
 }
 
 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)
+lldp_neighbor_id_get(NMLldpNeighbor *neighbor_nm,
+                     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,
+    r = nm_lldp_neighbor_get_chassis_id(neighbor_nm,
                                         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,
+    r = nm_lldp_neighbor_get_port_id(neighbor_nm,
                                      out_port_id_type,
                                      (gconstpointer *) out_port_id,
                                      out_port_id_len);
@@ -139,82 +135,16 @@ lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd,
 }
 
 static guint
-lldp_neighbor_id_hash(gconstpointer ptr)
+lldp_neighbor_id_hash(const LldpNeighbor *neigh)
 {
-    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);
+    return nm_lldp_neighbor_id_hash(nm_lldp_neighbor_get_id(neigh->neighbor_nm));
 }
 
 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;
+    return nm_lldp_neighbor_id_cmp(nm_lldp_neighbor_get_id(a->neighbor_nm),
+                                   nm_lldp_neighbor_get_id(b->neighbor_nm));
 }
 
 static int
@@ -225,7 +155,7 @@ lldp_neighbor_id_cmp_p(gconstpointer a, gconstpointer b, gpointer user_data)
 }
 
 static gboolean
-lldp_neighbor_id_equal(gconstpointer a, gconstpointer b)
+lldp_neighbor_id_equal(const LldpNeighbor *a, const LldpNeighbor *b)
 {
     return lldp_neighbor_id_cmp(a, b) == 0;
 }
@@ -239,7 +169,7 @@ lldp_neighbor_free(LldpNeighbor *neighbor)
     g_free(neighbor->chassis_id);
     g_free(neighbor->port_id);
     nm_g_variant_unref(neighbor->variant);
-    sd_lldp_neighbor_unref(neighbor->neighbor_sd);
+    nm_lldp_neighbor_unref(neighbor->neighbor_nm);
     nm_g_slice_free(neighbor);
 }
 
@@ -257,7 +187,7 @@ lldp_neighbor_equal(LldpNeighbor *a, LldpNeighbor *b)
     gsize         raw_len_a;
     gsize         raw_len_b;
 
-    if (a->neighbor_sd == b->neighbor_sd)
+    if (a->neighbor_nm == b->neighbor_nm)
         return TRUE;
 
     lldp_neighbor_get_raw(a, &raw_data_a, &raw_len_a);
@@ -293,7 +223,7 @@ parse_management_address_tlv(const uint8_t *data, gsize len)
     if (len < 11)
         return NULL;
 
-    nm_assert((data[0] >> 1) == SD_LLDP_TYPE_MGMT_ADDRESS);
+    nm_assert((data[0] >> 1) == NM_LLDP_TYPE_MGMT_ADDRESS);
     nm_assert((((data[0] & 1) << 8) + data[1]) + 2 == len);
 
     data += 2;
@@ -360,7 +290,7 @@ format_network_address(const guint8 *data, gsize sz)
     } else
         return NULL;
 
-    return nm_utils_inet_ntop_dup(family, &a);
+    return nm_inet_ntop_dup(family, &a);
 }
 
 static const char *
@@ -402,7 +332,7 @@ format_string_cp(const guint8 *data, gsize len, gboolean allow_trim)
 }
 
 static LldpNeighbor *
-lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
+lldp_neighbor_new(NMLldpNeighbor *neighbor_nm)
 {
     LldpNeighbor *neigh;
     guint8        chassis_id_type;
@@ -414,7 +344,7 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
     gs_free char *s_chassis_id = NULL;
     gs_free char *s_port_id    = NULL;
 
-    if (!lldp_neighbor_id_get(neighbor_sd,
+    if (!lldp_neighbor_id_get(neighbor_nm,
                               &chassis_id_type,
                               &chassis_id,
                               &chassis_id_len,
@@ -424,17 +354,17 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
         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:
+    case NM_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
+    case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
+    case NM_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
+    case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
+    case NM_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:
+    case NM_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:
+    case NM_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
         s_chassis_id = format_network_address(chassis_id, chassis_id_len);
         break;
     }
@@ -446,16 +376,16 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
     }
 
     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:
+    case NM_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
+    case NM_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
+    case NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
+    case NM_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:
+    case NM_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:
+    case NM_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
         s_port_id = format_network_address(port_id, port_id_len);
         break;
     }
@@ -468,7 +398,7 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
 
     neigh  = g_slice_new(LldpNeighbor);
     *neigh = (LldpNeighbor){
-        .neighbor_sd     = sd_lldp_neighbor_ref(neighbor_sd),
+        .neighbor_nm     = nm_lldp_neighbor_ref(neighbor_nm),
         .chassis_id_type = chassis_id_type,
         .chassis_id      = g_steal_pointer(&s_chassis_id),
         .port_id_type    = port_id_type,
@@ -480,15 +410,15 @@ lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
 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;
+    NMEtherAddr     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;
@@ -505,33 +435,33 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
     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);
+    r = nm_lldp_neighbor_get_destination_address(neigh->neighbor_nm, &destination_address);
     if (r < 0)
         str = NULL;
-    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE))
+    else if (nm_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))
+    else if (nm_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))
+    else if (nm_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)
+    if (nm_lldp_neighbor_get_port_description(neigh->neighbor_nm, &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)
+    if (nm_lldp_neighbor_get_system_name(neigh->neighbor_nm, &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)
+    if (nm_lldp_neighbor_get_system_description(neigh->neighbor_nm, &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)
+    if (nm_lldp_neighbor_get_system_capabilities(neigh->neighbor_nm, &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);
+    r = nm_lldp_neighbor_tlv_rewind(neigh->neighbor_nm);
     if (r < 0)
         nm_assert_not_reached();
     else {
@@ -556,14 +486,14 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
             guint8 type;
             guint8 subtype;
 
-            if (sd_lldp_neighbor_tlv_get_type(neigh->neighbor_sd, &type) < 0)
+            if (nm_lldp_neighbor_tlv_get_type(neigh->neighbor_nm, &type) < 0)
                 continue;
 
-            if (sd_lldp_neighbor_tlv_get_raw(neigh->neighbor_sd, (void *) &data8, &len) < 0)
+            if (nm_lldp_neighbor_tlv_get_raw(neigh->neighbor_nm, (void *) &data8, &len) < 0)
                 continue;
 
             switch (type) {
-            case SD_LLDP_TYPE_MGMT_ADDRESS:
+            case NM_LLDP_TYPE_MGMT_ADDRESS:
                 tmp_variant = parse_management_address_tlv(data8, len);
                 if (tmp_variant) {
                     if (!v_management_addresses_has) {
@@ -573,13 +503,13 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     g_variant_builder_add_value(&v_management_addresses, tmp_variant);
                 }
                 continue;
-            case SD_LLDP_TYPE_PRIVATE:
+            case NM_LLDP_TYPE_PRIVATE:
                 break;
             default:
                 continue;
             }
 
-            r = sd_lldp_neighbor_tlv_get_oui(neigh->neighbor_sd, oui, &subtype);
+            r = nm_lldp_neighbor_tlv_get_oui(neigh->neighbor_nm, oui, &subtype);
             if (r < 0) {
                 if (r == -ENXIO)
                     continue;
@@ -609,15 +539,15 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
             data8 += 6;
             len -= 6;
 
-            if (memcmp(oui, SD_LLDP_OUI_802_1, sizeof(oui)) == 0) {
+            if (memcmp(oui, NM_LLDP_OUI_802_1, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID:
+                case NM_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:
+                case NM_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID:
                     if (len != 3)
                         continue;
                     if (!v_ieee_802_1_ppvid) {
@@ -633,7 +563,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     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:
+                case NM_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME:
                 {
                     gs_free char *name_to_free = NULL;
                     const char   *name;
@@ -669,9 +599,9 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                 default:
                     continue;
                 }
-            } else if (memcmp(oui, SD_LLDP_OUI_802_3, sizeof(oui)) == 0) {
+            } else if (memcmp(oui, NM_LLDP_OUI_802_3, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
+                case NM_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
                     if (len != 5)
                         continue;
 
@@ -687,7 +617,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                         v_ieee_802_3_mac_phy_conf = g_variant_builder_end(&tmp_builder);
                     }
                     break;
-                case SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
+                case NM_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
                     if (len != 3)
                         continue;
 
@@ -703,7 +633,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                         v_ieee_802_3_power_via_mdi = g_variant_builder_end(&tmp_builder);
                     }
                     break;
-                case SD_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
+                case NM_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
                     if (len != 2)
                         continue;
                     if (!v_ieee_802_3_max_frame_size)
@@ -711,9 +641,9 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                             g_variant_new_uint32(unaligned_read_be16(data8));
                     break;
                 }
-            } else if (memcmp(oui, SD_LLDP_OUI_IANA, sizeof(oui)) == 0) {
+            } else if (memcmp(oui, NM_LLDP_OUI_IANA, sizeof(oui)) == 0) {
                 switch (subtype) {
-                case SD_LLDP_OUI_IANA_SUBTYPE_MUD:
+                case NM_LLDP_OUI_IANA_SUBTYPE_MUD:
                     if (!v_mud_url) {
                         gs_free char *s_free = NULL;
                         const char   *s;
@@ -725,7 +655,7 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
                     break;
                 }
             }
-        } while (sd_lldp_neighbor_tlv_next(neigh->neighbor_sd) > 0);
+        } while (nm_lldp_neighbor_tlv_next(neigh->neighbor_nm) > 0);
 
         if (v_management_addresses_has)
             nm_g_variant_builder_add_sv(&builder,
@@ -777,18 +707,17 @@ lldp_neighbor_to_variant(LldpNeighbor *neigh)
 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;
+    nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *neighbor_nm = NULL;
+    nm_auto(lldp_neighbor_freep) LldpNeighbor       *neigh       = NULL;
+    GVariant                                        *variant;
 
     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);
+    neighbor_nm = nm_lldp_neighbor_new_from_raw(raw_data, raw_len);
+    g_assert(neighbor_nm);
 
-    neigh = lldp_neighbor_new(neighbor_sd);
+    neigh = lldp_neighbor_new(neighbor_nm);
     g_assert(neigh);
 
     variant = lldp_neighbor_to_variant(neigh);
@@ -843,18 +772,18 @@ data_changed_schedule(NMLldpListener *self)
 }
 
 static void
-process_lldp_neighbor(NMLldpListener *self, sd_lldp_neighbor *neighbor_sd, gboolean remove)
+process_lldp_neighbor(NMLldpListener *self, NMLldpNeighbor *neighbor_nm, gboolean remove)
 {
     nm_auto(lldp_neighbor_freep) LldpNeighbor *neigh = NULL;
     LldpNeighbor                              *neigh_old;
 
     nm_assert(self);
-    nm_assert(self->lldp_handle);
+    nm_assert(self->lldp_rx);
     nm_assert(self->lldp_neighbors);
 
-    g_return_if_fail(neighbor_sd);
+    g_return_if_fail(neighbor_nm);
 
-    neigh = lldp_neighbor_new(neighbor_sd);
+    neigh = lldp_neighbor_new(neighbor_nm);
     if (!neigh) {
         _LOGT("process: failed to parse neighbor");
         return;
@@ -884,14 +813,17 @@ handle_changed:
 }
 
 static void
-lldp_event_handler(sd_lldp_rx *lldp, sd_lldp_rx_event_t event, sd_lldp_neighbor *n, void *userdata)
+lldp_event_handler(NMLldpRX *lldp, NMLldpRXEvent event, NMLldpNeighbor *n, void *user_data)
 {
-    process_lldp_neighbor(userdata,
+    NMLldpListener *self = user_data;
+
+    _LOGD("event: %s", nm_lldp_rx_event_to_string(event));
+    process_lldp_neighbor(self,
                           n,
                           !NM_IN_SET(event,
-                                     SD_LLDP_RX_EVENT_ADDED,
-                                     SD_LLDP_RX_EVENT_UPDATED,
-                                     SD_LLDP_RX_EVENT_REFRESHED));
+                                     NM_LLDP_RX_EVENT_ADDED,
+                                     NM_LLDP_RX_EVENT_UPDATED,
+                                     NM_LLDP_RX_EVENT_REFRESHED));
 }
 
 /*****************************************************************************/
@@ -935,35 +867,14 @@ nm_lldp_listener_new(int                  ifindex,
                      gpointer             notify_user_data,
                      GError             **error)
 {
-    NMLldpListener *self = NULL;
-    sd_lldp_rx     *lldp_handle;
-    int             r;
+    NMLldpListener                      *self    = NULL;
+    nm_auto(nm_lldp_rx_unrefp) NMLldpRX *lldp_rx = NULL;
+    int                                  r;
 
     g_return_val_if_fail(ifindex > 0, FALSE);
     g_return_val_if_fail(!error || !*error, FALSE);
     g_return_val_if_fail(notify_callback, FALSE);
 
-    r = sd_lldp_rx_new(&lldp_handle);
-    if (r < 0) {
-        g_set_error_literal(error,
-                            NM_DEVICE_ERROR,
-                            NM_DEVICE_ERROR_FAILED,
-                            "initialization failed");
-        return FALSE;
-    }
-
-    r = sd_lldp_rx_set_ifindex(lldp_handle, ifindex);
-    if (r < 0) {
-        g_set_error_literal(error,
-                            NM_DEVICE_ERROR,
-                            NM_DEVICE_ERROR_FAILED,
-                            "failed setting ifindex");
-        goto fail_handle;
-    }
-
-    r = sd_lldp_rx_set_neighbors_max(lldp_handle, MAX_NEIGHBORS);
-    nm_assert(r == 0);
-
     self  = g_slice_new(NMLldpListener);
     *self = (NMLldpListener){
         .ifindex          = ifindex,
@@ -971,39 +882,33 @@ nm_lldp_listener_new(int                  ifindex,
         .notify_user_data = notify_user_data,
     };
 
-    r = sd_lldp_rx_set_callback(lldp_handle, lldp_event_handler, self);
-    if (r < 0) {
-        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "set callback failed");
-        goto fail_handle;
-    }
+    nm_assert(nm_g_main_context_is_thread_default(g_main_context_default()));
 
-    r = sd_lldp_rx_attach_event(lldp_handle, NULL, 0);
-    if (r < 0) {
-        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "attach event failed");
-        goto fail_attached;
-    }
+    lldp_rx = nm_lldp_rx_new(&((NMLldpRXConfig){
+        .ifindex       = ifindex,
+        .neighbors_max = MAX_NEIGHBORS,
+        .callback      = lldp_event_handler,
+        .userdata      = self,
+    }));
 
-    r = sd_lldp_rx_start(lldp_handle);
+    r = nm_lldp_rx_start(lldp_rx);
     if (r < 0) {
         g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "start failed");
-        goto fail_attached;
+        goto fail;
     }
 
-    self->lldp_neighbors = g_hash_table_new_full(lldp_neighbor_id_hash,
-                                                 lldp_neighbor_id_equal,
+    self->lldp_neighbors = g_hash_table_new_full((GHashFunc) lldp_neighbor_id_hash,
+                                                 (GEqualFunc) lldp_neighbor_id_equal,
                                                  (GDestroyNotify) lldp_neighbor_free,
                                                  NULL);
-    self->lldp_handle    = lldp_handle;
+
+    self->lldp_rx = g_steal_pointer(&lldp_rx);
 
     _LOGD("start lldp listener");
     return self;
 
-fail_attached:
-    sd_lldp_rx_detach_event(lldp_handle);
-fail_handle:
-    if (self)
-        nm_g_slice_free(self);
-    sd_lldp_rx_unref(lldp_handle);
+fail:
+    nm_g_slice_free(self);
     return NULL;
 }
 
@@ -1012,9 +917,8 @@ nm_lldp_listener_destroy(NMLldpListener *self)
 {
     g_return_if_fail(self);
 
-    sd_lldp_rx_stop(self->lldp_handle);
-    sd_lldp_rx_detach_event(self->lldp_handle);
-    sd_lldp_rx_unref(self->lldp_handle);
+    nm_lldp_rx_stop(self->lldp_rx);
+    nm_lldp_rx_unref(self->lldp_rx);
 
     nm_clear_g_source_inst(&self->ratelimit_source);
 
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index 048afb02..7b319af3 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -16,6 +16,7 @@
 #include "nm-setting-connection.h"
 #include "nm-setting-ovs-bridge.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "libnm-core-intern/nm-core-internal.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceOvsBridge
@@ -124,13 +125,14 @@ nm_device_ovs_reapply_connection(NMDevice *self, NMConnection *con_old, NMConnec
         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));
+    nm_ovsdb_set_reapply(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),
+                         _nm_connection_get_setting(con_old, NM_TYPE_SETTING_OVS_OTHER_CONFIG),
+                         _nm_connection_get_setting(con_new, NM_TYPE_SETTING_OVS_OTHER_CONFIG));
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.h b/src/core/devices/ovs/nm-device-ovs-bridge.h
index 2b893343..51cc685d 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.h
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.h
@@ -8,7 +8,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.h b/src/core/devices/ovs/nm-device-ovs-interface.h
index 03b0e9a4..b8c951d2 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.h
+++ b/src/core/devices/ovs/nm-device-ovs-interface.h
@@ -8,7 +8,7 @@
 
 #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))
+    (_NM_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) \
diff --git a/src/core/devices/ovs/nm-device-ovs-port.h b/src/core/devices/ovs/nm-device-ovs-port.h
index f4516c01..87446fa4 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.h
+++ b/src/core/devices/ovs/nm-device-ovs-port.h
@@ -8,7 +8,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
index ff2c7858..50023778 100644
--- a/src/core/devices/ovs/nm-ovs-factory.c
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -27,7 +27,8 @@ typedef struct {
 } 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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
index 0d688396..85b7953f 100644
--- a/src/core/devices/ovs/nm-ovsdb.c
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -17,6 +17,7 @@
 #include "devices/nm-device.h"
 #include "nm-manager.h"
 #include "nm-setting-ovs-external-ids.h"
+#include "nm-setting-ovs-other-config.h"
 #include "nm-priv-helper-call.h"
 #include "libnm-platform/nm-platform.h"
 
@@ -24,18 +25,26 @@
 
 #define OVSDB_MAX_FAILURES 3
 
+#define OTHER_CONFIG_HWADDR "hwaddr"
+
 /*****************************************************************************/
 
 #if JANSSON_VERSION_HEX < 0x020400
 #warning "requires at least libjansson 2.4"
 #endif
 
+typedef enum {
+    STRDICT_TYPE_EXTERNAL_IDS,
+    STRDICT_TYPE_OTHER_CONFIG,
+} StrdictType;
+
 typedef struct {
     char      *port_uuid;
     char      *name;
     char      *connection_uuid;
     GPtrArray *interfaces; /* interface uuids */
     GArray    *external_ids;
+    GArray    *other_config;
 } OpenvswitchPort;
 
 typedef struct {
@@ -44,6 +53,7 @@ typedef struct {
     char      *connection_uuid;
     GPtrArray *ports; /* port uuids */
     GArray    *external_ids;
+    GArray    *other_config;
 } OpenvswitchBridge;
 
 typedef struct {
@@ -52,6 +62,7 @@ typedef struct {
     char   *type;
     char   *connection_uuid;
     GArray *external_ids;
+    GArray *other_config;
 } OpenvswitchInterface;
 
 /*****************************************************************************/
@@ -66,7 +77,7 @@ typedef enum {
     OVSDB_ADD_INTERFACE,
     OVSDB_DEL_INTERFACE,
     OVSDB_SET_INTERFACE_MTU,
-    OVSDB_SET_EXTERNAL_IDS,
+    OVSDB_SET_REAPPLY,
 } OvsdbCommand;
 
 #define CALL_ID_UNSPEC G_MAXUINT64
@@ -92,9 +103,11 @@ typedef union {
         NMDeviceType device_type;
         char        *ifname;
         char        *connection_uuid;
-        GHashTable  *exid_old;
-        GHashTable  *exid_new;
-    } set_external_ids;
+        GHashTable  *external_ids_old;
+        GHashTable  *external_ids_new;
+        GHashTable  *other_config_old;
+        GHashTable  *other_config_new;
+    } set_reapply;
 } OvsdbMethodPayload;
 
 typedef struct {
@@ -223,20 +236,24 @@ static void cleanup_check_ready(NMOvsdb *self);
             },                                                    \
     }))
 
-#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),                                     \
-            },                                                                      \
+#define OVSDB_METHOD_PAYLOAD_SET_REAPPLY(xdevice_type,                               \
+                                         xifname,                                    \
+                                         xconnection_uuid,                           \
+                                         xexternal_ids_old,                          \
+                                         xexternal_ids_new,                          \
+                                         xother_config_old,                          \
+                                         xother_config_new)                          \
+    (&((const OvsdbMethodPayload){                                                   \
+        .set_reapply =                                                               \
+            {                                                                        \
+                .device_type      = xdevice_type,                                    \
+                .ifname           = (char *) NM_CONSTCAST(char, (xifname)),          \
+                .connection_uuid  = (char *) NM_CONSTCAST(char, (xconnection_uuid)), \
+                .external_ids_old = (xexternal_ids_old),                             \
+                .external_ids_new = (xexternal_ids_new),                             \
+                .other_config_old = (xother_config_old),                             \
+                .other_config_new = (xother_config_new),                             \
+            },                                                                       \
     }))
 
 /*****************************************************************************/
@@ -291,11 +308,13 @@ _call_complete(OvsdbMethodCall *call, json_t *response, GError *error)
     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);
+    case OVSDB_SET_REAPPLY:
+        nm_clear_g_free(&call->payload.set_reapply.ifname);
+        nm_clear_g_free(&call->payload.set_reapply.connection_uuid);
+        nm_clear_pointer(&call->payload.set_reapply.external_ids_old, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.external_ids_new, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.other_config_old, g_hash_table_destroy);
+        nm_clear_pointer(&call->payload.set_reapply.other_config_new, g_hash_table_destroy);
         break;
     }
 
@@ -312,6 +331,7 @@ _free_bridge(OpenvswitchBridge *ovs_bridge)
     g_free(ovs_bridge->connection_uuid);
     g_ptr_array_free(ovs_bridge->ports, TRUE);
     nm_g_array_unref(ovs_bridge->external_ids);
+    nm_g_array_unref(ovs_bridge->other_config);
     nm_g_slice_free(ovs_bridge);
 }
 
@@ -323,6 +343,7 @@ _free_port(OpenvswitchPort *ovs_port)
     g_free(ovs_port->connection_uuid);
     g_ptr_array_free(ovs_port->interfaces, TRUE);
     nm_g_array_unref(ovs_port->external_ids);
+    nm_g_array_unref(ovs_port->other_config);
     nm_g_slice_free(ovs_port);
 }
 
@@ -334,6 +355,7 @@ _free_interface(OpenvswitchInterface *ovs_interface)
     g_free(ovs_interface->connection_uuid);
     g_free(ovs_interface->type);
     nm_g_array_unref(ovs_interface->external_ids);
+    nm_g_array_unref(ovs_interface->other_config);
     nm_g_slice_free(ovs_interface);
 }
 
@@ -447,19 +469,22 @@ ovsdb_call_method(NMOvsdb                  *self,
                    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);
+    case OVSDB_SET_REAPPLY:
+        call->payload.set_reapply.device_type     = payload->set_reapply.device_type;
+        call->payload.set_reapply.ifname          = g_strdup(payload->set_reapply.ifname);
+        call->payload.set_reapply.connection_uuid = g_strdup(payload->set_reapply.connection_uuid);
+        call->payload.set_reapply.external_ids_old =
+            nm_g_hash_table_ref(payload->set_reapply.external_ids_old);
+        call->payload.set_reapply.external_ids_new =
+            nm_g_hash_table_ref(payload->set_reapply.external_ids_new);
+        call->payload.set_reapply.other_config_old =
+            nm_g_hash_table_ref(payload->set_reapply.other_config_old);
+        call->payload.set_reapply.other_config_new =
+            nm_g_hash_table_ref(payload->set_reapply.other_config_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);
+                   "new: set external-ids/other-config con-uuid=%s, interface=%s",
+                   call->payload.set_reapply.connection_uuid,
+                   call->payload.set_reapply.ifname);
         break;
     }
 
@@ -604,12 +629,12 @@ _set_bridge_mac(json_t *params, const char *ifname, const char *mac)
                   "other_config",
                   "delete",
                   "set",
-                  "hwaddr",
+                  OTHER_CONFIG_HWADDR,
 
                   "other_config",
                   "insert",
                   "map",
-                  "hwaddr",
+                  OTHER_CONFIG_HWADDR,
                   mac,
 
                   "where",
@@ -677,92 +702,155 @@ _set_port_interfaces(json_t *params, const char *ifname, json_t *new_interfaces)
 }
 
 static json_t *
-_j_create_external_ids_array_new(NMConnection *connection)
+_j_create_strdict_new(NMConnection *connection,
+                      StrdictType   strdict_type,
+                      const char   *other_config_hwaddr)
 {
+    NMSettingOvsOtherConfig *s_other_config = NULL;
+    NMSettingOvsExternalIDs *s_external_ids = NULL;
     json_t                  *array;
-    const char *const       *external_ids   = NULL;
-    guint                    n_external_ids = 0;
+    const char *const       *strv   = NULL;
+    guint                    n_strv = 0;
     guint                    i;
     const char              *uuid;
-    NMSettingOvsExternalIDs *s_exid;
 
     nm_assert(NM_IS_CONNECTION(connection));
+    nm_assert(NM_IN_SET(strdict_type, STRDICT_TYPE_EXTERNAL_IDS, STRDICT_TYPE_OTHER_CONFIG));
 
     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));
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        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));
+    } else {
+        if (other_config_hwaddr) {
+            json_array_append_new(array,
+                                  json_pack("[s, s]", OTHER_CONFIG_HWADDR, other_config_hwaddr));
+        }
+    }
+
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        s_external_ids = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_EXTERNAL_IDS);
+        if (s_external_ids)
+            strv = nm_setting_ovs_external_ids_get_data_keys(s_external_ids, &n_strv);
+    } else {
+        s_other_config = _nm_connection_get_setting(connection, NM_TYPE_SETTING_OVS_OTHER_CONFIG);
+        if (s_other_config)
+            strv = nm_setting_ovs_other_config_get_data_keys(s_other_config, &n_strv);
+    }
+
+    for (i = 0; i < n_strv; i++) {
+        const char *k = strv[i];
 
-    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];
+        if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && other_config_hwaddr
+            && nm_streq(k, OTHER_CONFIG_HWADDR)) {
+            /* "hwaddr" is explicitly overwritten. */
+            continue;
+        }
 
         json_array_append_new(
             array,
-            json_pack("[s, s]", k, nm_setting_ovs_external_ids_get_data(s_exid, k)));
+            json_pack("[s, s]",
+                      k,
+                      strdict_type == STRDICT_TYPE_EXTERNAL_IDS
+                          ? nm_setting_ovs_external_ids_get_data(s_external_ids, k)
+                          : nm_setting_ovs_other_config_get_data(s_other_config, 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)
+static void
+_j_create_strv_array_update(json_t     *mutations,
+                            StrdictType strdict_type,
+                            const char *connection_uuid,
+                            GHashTable *hash_old,
+                            GHashTable *hash_new)
 {
     GHashTableIter iter;
-    json_t        *mutations;
     json_t        *array;
     const char    *key;
     const char    *val;
 
-    nm_assert(connection_uuid);
+    /* This is called during reapply. We accept reapplying all settings,
+     * except other_config:hwaddr. That one cannot change and is specially
+     * handled below. The reason is that we knew the correct "hwaddr" during
+     * _j_create_strdict_new(), but we don't do now. At least not easily,
+     * and it's not clear that reapply of the MAC address is really useful. */
 
-    mutations = json_array();
+    nm_assert((!!connection_uuid) == (strdict_type == STRDICT_TYPE_EXTERNAL_IDS));
+    nm_assert(NM_IN_SET(strdict_type, STRDICT_TYPE_EXTERNAL_IDS, STRDICT_TYPE_OTHER_CONFIG));
 
-    array = json_array();
-    if (exid_old) {
-        g_hash_table_iter_init(&iter, exid_old);
+    array = NULL;
+    if (hash_old) {
+        g_hash_table_iter_init(&iter, hash_old);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) {
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
+                continue;
+            if (!array)
+                array = json_array();
             json_array_append_new(array, json_string(key));
         }
     }
-    if (exid_new) {
-        g_hash_table_iter_init(&iter, exid_new);
+    if (hash_new) {
+        g_hash_table_iter_init(&iter, hash_new);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, NULL)) {
-            if (nm_g_hash_table_contains(exid_old, key))
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
                 continue;
+            if (nm_g_hash_table_contains(hash_old, key))
+                continue;
+            if (!array)
+                array = json_array();
             json_array_append_new(array, json_string(key));
         }
     }
-    if (!nm_g_hash_table_contains(exid_old, NM_OVS_EXTERNAL_ID_NM_PREFIX)
-        && !nm_g_hash_table_contains(exid_new, NM_OVS_EXTERNAL_ID_NM_PREFIX)) {
-        json_array_append_new(array, json_string(NM_OVS_EXTERNAL_ID_NM_PREFIX));
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        if (!nm_g_hash_table_contains(hash_old, NM_OVS_EXTERNAL_ID_NM_PREFIX)
+            && !nm_g_hash_table_contains(hash_new, NM_OVS_EXTERNAL_ID_NM_PREFIX)) {
+            if (!array)
+                array = json_array();
+            json_array_append_new(array, json_string(NM_OVS_EXTERNAL_ID_NM_PREFIX));
+        }
+    }
+    if (array) {
+        json_array_append_new(
+            mutations,
+            json_pack("[s, s, [s, o]]",
+                      strdict_type == STRDICT_TYPE_EXTERNAL_IDS ? "external_ids" : "other_config",
+                      "delete",
+                      "set",
+                      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);
+    if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+        json_array_append_new(
+            array,
+            json_pack("[s, s]", NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID, connection_uuid));
+    }
+    if (hash_new) {
+        g_hash_table_iter_init(&iter, hash_new);
         while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &val)) {
-            if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+            if (strdict_type == STRDICT_TYPE_EXTERNAL_IDS) {
+                if (NM_STR_HAS_PREFIX(key, NM_OVS_EXTERNAL_ID_NM_PREFIX))
+                    continue;
+            }
+            if (strdict_type == STRDICT_TYPE_OTHER_CONFIG && nm_streq(key, OTHER_CONFIG_HWADDR))
                 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;
+    json_array_append_new(
+        mutations,
+        json_pack("[s, s, [s, o]]",
+                  strdict_type == STRDICT_TYPE_EXTERNAL_IDS ? "external_ids" : "other_config",
+                  "insert",
+                  "map",
+                  array));
 }
 
 /**
@@ -779,18 +867,19 @@ _insert_interface(json_t       *params,
     const char            *type = NULL;
     NMSettingOvsInterface *s_ovs_iface;
     NMSettingOvsDpdk      *s_ovs_dpdk;
-    const char            *devargs;
-    guint32                n_rxq;
     char                   sbuf[64];
     json_t                *dpdk_array;
     NMSettingOvsPatch     *s_ovs_patch;
     json_t                *options = json_array();
     json_t                *row;
-    guint32                mtu = 0;
+    guint32                mtu            = 0;
+    guint32                ofport_request = 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 (s_ovs_iface) {
+        type           = nm_setting_ovs_interface_get_interface_type(s_ovs_iface);
+        ofport_request = nm_setting_ovs_interface_get_ofport_request(s_ovs_iface);
+    }
 
     if (nm_streq0(type, "internal")) {
         NMSettingWired *s_wired;
@@ -808,8 +897,15 @@ _insert_interface(json_t       *params,
         s_ovs_patch = nm_connection_get_setting_ovs_patch(interface);
 
     if (s_ovs_dpdk) {
-        devargs = nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk);
-        n_rxq   = nm_setting_ovs_dpdk_get_n_rxq(s_ovs_dpdk);
+        const char *devargs;
+        guint32     n_rxq;
+        guint32     n_rxq_desc;
+        guint32     n_txq_desc;
+
+        devargs    = nm_setting_ovs_dpdk_get_devargs(s_ovs_dpdk);
+        n_rxq      = nm_setting_ovs_dpdk_get_n_rxq(s_ovs_dpdk);
+        n_rxq_desc = nm_setting_ovs_dpdk_get_n_rxq_desc(s_ovs_dpdk);
+        n_txq_desc = nm_setting_ovs_dpdk_get_n_txq_desc(s_ovs_dpdk);
 
         dpdk_array = json_array();
 
@@ -820,6 +916,16 @@ _insert_interface(json_t       *params,
             json_array_append_new(dpdk_array,
                                   json_pack("[s,s]", "n_rxq", nm_sprintf_buf(sbuf, "%u", n_rxq)));
         }
+        if (n_rxq_desc != 0) {
+            json_array_append_new(
+                dpdk_array,
+                json_pack("[s,s]", "n_rxq_desc", nm_sprintf_buf(sbuf, "%u", n_rxq_desc)));
+        }
+        if (n_txq_desc != 0) {
+            json_array_append_new(
+                dpdk_array,
+                json_pack("[s,s]", "n_txq_desc", nm_sprintf_buf(sbuf, "%u", n_txq_desc)));
+        }
 
         json_array_append_new(options, dpdk_array);
 
@@ -831,7 +937,7 @@ _insert_interface(json_t       *params,
         json_array_append_new(options, json_array());
     }
 
-    row = json_pack("{s:s, s:s, s:o, s:o}",
+    row = json_pack("{s:s, s:s, s:o, s:o, s:o}",
                     "name",
                     nm_connection_get_interface_name(interface),
                     "type",
@@ -839,7 +945,9 @@ _insert_interface(json_t       *params,
                     "options",
                     options,
                     "external_ids",
-                    _j_create_external_ids_array_new(interface));
+                    _j_create_strdict_new(interface, STRDICT_TYPE_EXTERNAL_IDS, NULL),
+                    "other_config",
+                    _j_create_strdict_new(interface, STRDICT_TYPE_OTHER_CONFIG, NULL));
 
     if (cloned_mac)
         json_object_set_new(row, "mac", json_string(cloned_mac));
@@ -847,6 +955,9 @@ _insert_interface(json_t       *params,
     if (mtu != 0)
         json_object_set_new(row, "mtu_request", json_integer(mtu));
 
+    if (ofport_request != 0)
+        json_object_set_new(row, "ofport_request", json_integer(ofport_request));
+
     json_array_append_new(params,
                           json_pack("{s:s, s:s, s:o, s:s}",
                                     "op",
@@ -869,6 +980,7 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
 {
     NMSettingOvsPort *s_ovs_port;
     const char       *vlan_mode      = NULL;
+    json_t           *trunks         = NULL;
     guint             tag            = 0;
     const char       *lacp           = NULL;
     const char       *bond_mode      = NULL;
@@ -881,18 +993,34 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
     row = json_object();
 
     if (s_ovs_port) {
+        const GPtrArray *ranges;
+        guint            i;
+        guint64          start;
+        guint64          end;
+
         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);
+
+        ranges = _nm_setting_ovs_port_get_trunks_arr(s_ovs_port);
+        for (i = 0; i < ranges->len; i++) {
+            if (!trunks)
+                trunks = json_array();
+            nm_range_get_range(ranges->pdata[i], &start, &end);
+            for (; start <= end; start++)
+                json_array_append_new(trunks, json_integer(start));
+        }
     }
 
     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 (trunks)
+        json_object_set_new(row, "trunks", json_pack("[s, o]", "set", trunks));
     if (lacp)
         json_object_set_new(row, "lacp", json_string(lacp));
     if (bond_mode)
@@ -904,7 +1032,12 @@ _insert_port(json_t *params, NMConnection *port, json_t *new_interfaces)
 
     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));
+    json_object_set_new(row,
+                        "external_ids",
+                        _j_create_strdict_new(port, STRDICT_TYPE_EXTERNAL_IDS, NULL));
+    json_object_set_new(row,
+                        "other_config",
+                        _j_create_strdict_new(port, STRDICT_TYPE_OTHER_CONFIG, NULL));
 
     /* Create a new one. */
     json_array_append_new(params,
@@ -964,13 +1097,12 @@ _insert_bridge(json_t       *params,
 
     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));
-    }
+    json_object_set_new(row,
+                        "external_ids",
+                        _j_create_strdict_new(bridge, STRDICT_TYPE_EXTERNAL_IDS, NULL));
+    json_object_set_new(row,
+                        "other_config",
+                        _j_create_strdict_new(bridge, STRDICT_TYPE_OTHER_CONFIG, cloned_mac));
 
     /* Create a new one. */
     json_array_append_new(params,
@@ -1332,9 +1464,9 @@ ovsdb_next_command(NMOvsdb *self)
     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:[s, s, s, s]}],"
+                        "  s:[{s:[s, s, s, s, s]}],"
                         "  s:[{s:[]}]"
                         "}]}",
                         "id",
@@ -1348,16 +1480,19 @@ ovsdb_next_command(NMOvsdb *self)
                         "name",
                         "ports",
                         "external_ids",
+                        "other_config",
                         "Port",
                         "columns",
                         "name",
                         "interfaces",
                         "external_ids",
+                        "other_config",
                         "Interface",
                         "columns",
                         "name",
                         "type",
                         "external_ids",
+                        "other_config",
                         "error",
                         "Open_vSwitch",
                         "columns");
@@ -1398,24 +1533,38 @@ ovsdb_next_command(NMOvsdb *self)
                                             "==",
                                             call->payload.set_interface_mtu.ifname));
             break;
-        case OVSDB_SET_EXTERNAL_IDS:
+        case OVSDB_SET_REAPPLY:
+        {
+            json_t *mutations;
+
+            mutations = json_array();
+
+            _j_create_strv_array_update(mutations,
+                                        STRDICT_TYPE_EXTERNAL_IDS,
+                                        call->payload.set_reapply.connection_uuid,
+                                        call->payload.set_reapply.external_ids_old,
+                                        call->payload.set_reapply.external_ids_new);
+            _j_create_strv_array_update(mutations,
+                                        STRDICT_TYPE_OTHER_CONFIG,
+                                        NULL,
+                                        call->payload.set_reapply.other_config_old,
+                                        call->payload.set_reapply.other_config_new);
+
             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),
+                          _device_type_to_table(call->payload.set_reapply.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),
+                          mutations,
                           "where",
                           "name",
                           "==",
-                          call->payload.set_external_ids.ifname));
+                          call->payload.set_reapply.ifname));
             break;
+        }
 
         default:
             nm_assert_not_reached();
@@ -1500,19 +1649,18 @@ _uuids_to_array(const json_t *items)
 }
 
 static void
-_external_ids_extract(json_t *external_ids, GArray **out_array, const char **out_connection_uuid)
+_strdict_extract(json_t *strdict, GArray **out_array)
 {
     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))))
+    if (!nm_streq0("map", json_string_value(json_array_get(strdict, 0))))
         return;
 
-    array = json_array_get(external_ids, 1);
+    array = json_array_get(strdict, 1);
 
     json_array_foreach (array, index, value) {
         const char        *key = json_string_value(json_array_get(value, 0));
@@ -1533,16 +1681,26 @@ _external_ids_extract(json_t *external_ids, GArray **out_array, const char **out
             .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 const char *
+_strdict_find_key(GArray *array, const char *key)
+{
+    gssize idx;
+
+    idx = nm_utils_named_value_list_find(nm_g_array_first_p(array, NMUtilsNamedValue),
+                                         nm_g_array_len(array),
+                                         key,
+                                         FALSE);
+    if (idx < 0)
+        return NULL;
+
+    return nm_g_array_index(array, NMUtilsNamedValue, idx).value_str;
+}
+
 static gboolean
-_external_ids_equal(const GArray *arr1, const GArray *arr2)
+_strdict_equals(const GArray *arr1, const GArray *arr2)
 {
     guint n;
     guint i;
@@ -1552,19 +1710,19 @@ _external_ids_equal(const GArray *arr1, const GArray *arr2)
     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);
+        const NMUtilsNamedValue *n1 = &nm_g_array_index(arr1, NMUtilsNamedValue, i);
+        const NMUtilsNamedValue *n2 = &nm_g_array_index(arr2, NMUtilsNamedValue, i);
 
-        if (!nm_streq0(n1->name, n2->name))
+        if (!nm_streq(n1->name, n2->name))
             return FALSE;
-        if (!nm_streq0(n1->value_str, n2->value_str))
+        if (!nm_streq(n1->value_str, n2->value_str))
             return FALSE;
     }
     return TRUE;
 }
 
 static char *
-_external_ids_to_string(const GArray *arr)
+_strdict_to_string(const GArray *arr)
 {
     NMStrBuf strbuf;
     guint    i;
@@ -1575,7 +1733,7 @@ _external_ids_to_string(const GArray *arr)
     strbuf = NM_STR_BUF_INIT(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);
+        const NMUtilsNamedValue *n = &nm_g_array_index(arr, NMUtilsNamedValue, i);
 
         if (i > 0)
             nm_str_buf_append_c(&strbuf, ',');
@@ -1605,8 +1763,9 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
     json_t         *interface = NULL;
     json_t         *items;
     json_t         *external_ids;
+    json_t         *other_config;
     json_error_t    json_error = {
-           0,
+        0,
     };
     void       *iter;
     const char *name;
@@ -1644,12 +1803,13 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
     json_object_foreach (interface, key, value) {
         OpenvswitchInterface  *ovs_interface;
         gs_unref_array GArray *external_ids_arr = NULL;
+        gs_unref_array GArray *other_config_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}}",
+                        "{s:{s:s, s:s, s?:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
@@ -1658,7 +1818,9 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                         "error",
                         &error,
                         "external_ids",
-                        &external_ids);
+                        &external_ids,
+                        "other_config",
+                        &other_config);
         if (r != 0) {
             gpointer unused;
 
@@ -1701,7 +1863,10 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_interface, _free_interface);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
 
         if (ovs_interface) {
             gboolean changed = FALSE;
@@ -1710,14 +1875,20 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
 
             changed |= nm_strdup_reset(&ovs_interface->type, type);
             changed |= nm_strdup_reset(&ovs_interface->connection_uuid, connection_uuid);
-            if (!_external_ids_equal(ovs_interface->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_interface->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_interface->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_interface->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_interface->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s",
+                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s, "
+                      "other-config=%s",
                       key,
                       type,
                       ovs_interface->name,
@@ -1725,10 +1896,12 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                                            ", ",
                                            ovs_interface->connection_uuid,
                                            ""),
-                      (strtmp = _external_ids_to_string(ovs_interface->external_ids)));
+                      (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_interface  = g_slice_new(OpenvswitchInterface);
             *ovs_interface = (OpenvswitchInterface){
@@ -1737,17 +1910,20 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .type            = g_strdup(type),
                 .connection_uuid = g_strdup(connection_uuid),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_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)));
+            _LOGT(
+                "obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s, other-config=%s",
+                key,
+                ovs_interface->type,
+                ovs_interface->name,
+                NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                     ", ",
+                                     ovs_interface->connection_uuid,
+                                     ""),
+                (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             _signal_emit_device_added(self,
                                       ovs_interface->name,
                                       NM_DEVICE_TYPE_OVS_INTERFACE,
@@ -1768,16 +1944,19 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
         gs_unref_ptrarray GPtrArray *interfaces = NULL;
         OpenvswitchPort             *ovs_port;
         gs_unref_array GArray       *external_ids_arr = NULL;
+        gs_unref_array GArray       *other_config_arr = NULL;
         const char                  *connection_uuid  = NULL;
         int                          r;
 
         r = json_unpack(value,
-                        "{s:{s:s, s:o, s:o}}",
+                        "{s:{s:s, s:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
                         "external_ids",
                         &external_ids,
+                        "other_config",
+                        &other_config,
                         "interfaces",
                         &items);
         if (r != 0) {
@@ -1811,7 +1990,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_port, _free_port);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
+
         interfaces = _uuids_to_array(items);
 
         if (ovs_port) {
@@ -1825,24 +2008,31 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 NM_SWAP(&ovs_port->interfaces, &interfaces);
                 changed = TRUE;
             }
-            if (!_external_ids_equal(ovs_port->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_port->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_port->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_port->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_port->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s",
+                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s, other-config=%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)));
+                      (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_port->other_config)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_port  = g_slice_new(OpenvswitchPort);
             *ovs_port = (OpenvswitchPort){
@@ -1851,16 +2041,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .connection_uuid = g_strdup(connection_uuid),
                 .interfaces      = g_steal_pointer(&interfaces),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->ports, ovs_port);
-            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s",
+            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s, other-config=%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)));
+                  (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
+                  (strtmp2 = _strdict_to_string(ovs_port->other_config)));
             _signal_emit_device_added(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
         }
     }
@@ -1869,16 +2061,19 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
         gs_unref_ptrarray GPtrArray *ports = NULL;
         OpenvswitchBridge           *ovs_bridge;
         gs_unref_array GArray       *external_ids_arr = NULL;
+        gs_unref_array GArray       *other_config_arr = NULL;
         const char                  *connection_uuid  = NULL;
         int                          r;
 
         r = json_unpack(value,
-                        "{s:{s:s, s:o, s:o}}",
+                        "{s:{s:s, s:o, s:o, s:o}}",
                         "new",
                         "name",
                         &name,
                         "external_ids",
                         &external_ids,
+                        "other_config",
+                        &other_config,
                         "ports",
                         &items);
 
@@ -1916,7 +2111,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             nm_clear_pointer(&ovs_bridge, _free_bridge);
         }
 
-        _external_ids_extract(external_ids, &external_ids_arr, &connection_uuid);
+        _strdict_extract(external_ids, &external_ids_arr);
+        connection_uuid =
+            _strdict_find_key(external_ids_arr, NM_OVS_EXTERNAL_ID_NM_CONNECTION_UUID);
+        _strdict_extract(other_config, &other_config_arr);
+
         ports = _uuids_to_array(items);
 
         if (ovs_bridge) {
@@ -1930,24 +2129,31 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 NM_SWAP(&ovs_bridge->ports, &ports);
                 changed = TRUE;
             }
-            if (!_external_ids_equal(ovs_bridge->external_ids, external_ids_arr)) {
+            if (!_strdict_equals(ovs_bridge->external_ids, external_ids_arr)) {
                 NM_SWAP(&ovs_bridge->external_ids, &external_ids_arr);
                 changed = TRUE;
             }
+            if (!_strdict_equals(ovs_bridge->other_config, other_config_arr)) {
+                NM_SWAP(&ovs_bridge->other_config, &other_config_arr);
+                changed = TRUE;
+            }
             if (changed) {
-                gs_free char *strtmp = NULL;
+                gs_free char *strtmp1 = NULL;
+                gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s",
+                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s, other-config=%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)));
+                      (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
+                      (strtmp2 = _strdict_to_string(ovs_bridge->external_ids)));
             }
         } else {
-            gs_free char *strtmp = NULL;
+            gs_free char *strtmp1 = NULL;
+            gs_free char *strtmp2 = NULL;
 
             ovs_bridge  = g_slice_new(OpenvswitchBridge);
             *ovs_bridge = (OpenvswitchBridge){
@@ -1956,16 +2162,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .connection_uuid = g_strdup(connection_uuid),
                 .ports           = g_steal_pointer(&ports),
                 .external_ids    = g_steal_pointer(&external_ids_arr),
+                .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->bridges, ovs_bridge);
-            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s",
+            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s, other-config=%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)));
+                  (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
+                  (strtmp2 = _strdict_to_string(ovs_bridge->other_config)));
             _signal_emit_device_added(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
         }
     }
@@ -2007,7 +2215,7 @@ ovsdb_got_msg(NMOvsdb *self, json_t *msg)
 {
     NMOvsdbPrivate *priv       = NM_OVSDB_GET_PRIVATE(self);
     json_error_t    json_error = {
-           0,
+        0,
     };
     json_t     *json_id = NULL;
     json_int_t  id      = (json_int_t) -1;
@@ -2149,7 +2357,7 @@ ovsdb_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
     gssize          size;
     json_t         *msg;
     json_error_t    json_error = {
-           0,
+        0,
     };
 
     size = g_input_stream_read_finish(stream, res, &error);
@@ -2703,31 +2911,50 @@ nm_ovsdb_set_interface_mtu(NMOvsdb        *self,
 }
 
 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)
+nm_ovsdb_set_reapply(NMOvsdb                 *self,
+                     NMDeviceType             device_type,
+                     const char              *ifname,
+                     const char              *connection_uuid,
+                     NMSettingOvsExternalIDs *s_external_ids_old,
+                     NMSettingOvsExternalIDs *s_external_ids_new,
+                     NMSettingOvsOtherConfig *s_other_config_old,
+                     NMSettingOvsOtherConfig *s_other_config_new)
 {
-    gs_unref_hashtable GHashTable *exid_old = NULL;
-    gs_unref_hashtable GHashTable *exid_new = NULL;
-
-    exid_old =
-        s_exid_old ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_old)) : NULL;
-    exid_new =
-        s_exid_new ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_exid_new)) : NULL;
+    gs_unref_hashtable GHashTable *external_ids_old = NULL;
+    gs_unref_hashtable GHashTable *external_ids_new = NULL;
+    gs_unref_hashtable GHashTable *other_config_old = NULL;
+    gs_unref_hashtable GHashTable *other_config_new = NULL;
+
+    external_ids_old =
+        s_external_ids_old
+            ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_external_ids_old))
+            : NULL;
+    external_ids_new =
+        s_external_ids_new
+            ? nm_strdict_clone(_nm_setting_ovs_external_ids_get_data(s_external_ids_new))
+            : NULL;
+
+    other_config_old =
+        s_other_config_old
+            ? nm_strdict_clone(_nm_setting_ovs_other_config_get_data(s_other_config_old))
+            : NULL;
+    other_config_new =
+        s_other_config_new
+            ? nm_strdict_clone(_nm_setting_ovs_other_config_get_data(s_other_config_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));
+                      OVSDB_SET_REAPPLY,
+                      OVSDB_METHOD_PAYLOAD_SET_REAPPLY(device_type,
+                                                       ifname,
+                                                       connection_uuid,
+                                                       external_ids_old,
+                                                       external_ids_new,
+                                                       other_config_old,
+                                                       other_config_new));
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/ovs/nm-ovsdb.h b/src/core/devices/ovs/nm-ovsdb.h
index a50db45a..a022ff00 100644
--- a/src/core/devices/ovs/nm-ovsdb.h
+++ b/src/core/devices/ovs/nm-ovsdb.h
@@ -7,7 +7,7 @@
 #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(obj)            (_NM_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))
@@ -47,14 +47,14 @@ void nm_ovsdb_set_interface_mtu(NMOvsdb        *self,
                                 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);
+void nm_ovsdb_set_reapply(NMOvsdb                 *self,
+                          NMDeviceType             device_type,
+                          const char              *ifname,
+                          const char              *connection_uuid,
+                          NMSettingOvsExternalIDs *s_external_ids_old,
+                          NMSettingOvsExternalIDs *s_external_ids_new,
+                          NMSettingOvsOtherConfig *s_other_config_old,
+                          NMSettingOvsOtherConfig *s_other_config_new);
 
 gboolean nm_ovsdb_is_ready(NMOvsdb *self);
 
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index 9eca008a..3e3fe52b 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -144,6 +144,7 @@ _update_port_config(NMDeviceTeam *self, const char *port_iface, const char *sani
     NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
     int                  err;
 
+    _LOGT(LOGD_TEAM, "setting port config: %s", sanitized_config);
     err = teamdctl_port_config_update_raw(priv->tdc, port_iface, sanitized_config);
     if (err != 0) {
         _LOGE(LOGD_TEAM, "failed to update config for port %s (err=%d)", port_iface, err);
@@ -154,23 +155,20 @@ _update_port_config(NMDeviceTeam *self, const char *port_iface, const char *sani
 }
 
 static gboolean
-ensure_teamd_connection(NMDevice *device)
+ensure_teamd_connection(NMDevice *device, GError **error)
 {
-    NMDeviceTeam         *self  = NM_DEVICE_TEAM(device);
-    NMDeviceTeamPrivate  *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
-    gs_free_error GError *error = NULL;
-    const char           *port_iface;
-    const char           *port_config;
-    GHashTableIter        iter;
+    NMDeviceTeam        *self = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    const char          *port_iface;
+    const char          *port_config;
+    GHashTableIter       iter;
 
     if (priv->tdc)
         return TRUE;
 
-    priv->tdc = _tdc_connect_new(self, nm_device_get_iface(device), &error);
-    if (!priv->tdc) {
-        _LOGE(LOGD_TEAM, "failed to connect to teamd: %s", error->message);
+    priv->tdc = _tdc_connect_new(self, nm_device_get_iface(device), error);
+    if (!priv->tdc)
         return FALSE;
-    }
 
     g_hash_table_iter_init(&iter, priv->port_configs);
     while (g_hash_table_iter_next(&iter, (gpointer *) &port_iface, (gpointer *) &port_config))
@@ -229,10 +227,17 @@ update_connection(NMDevice *device, NMConnection *connection)
     NMSettingTeam       *s_team = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_TEAM);
     NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
     struct teamdctl     *tdc    = priv->tdc;
+    GError              *error  = NULL;
 
     /* Read the configuration only if not already set */
-    if (!priv->config && ensure_teamd_connection(device))
-        teamd_read_config(self);
+    if (!priv->config) {
+        if (ensure_teamd_connection(device, &error)) {
+            teamd_read_config(self);
+        } else {
+            _LOGD(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+            g_clear_error(&error);
+        }
+    }
 
     /* Restore previous tdc state */
     if (priv->tdc && !tdc) {
@@ -258,9 +263,10 @@ master_update_slave_connection(NMDevice     *device,
     gs_free_error GError *connect_error = NULL;
     int                   err           = 0;
     struct teamdctl      *tdc;
-    const char           *team_port_config = NULL;
-    const char           *iface            = nm_device_get_iface(device);
-    const char           *iface_slave      = nm_device_get_iface(slave);
+    const char           *team_port_config   = NULL;
+    const char           *iface              = nm_device_get_iface(device);
+    const char           *iface_slave        = nm_device_get_iface(slave);
+    NMConnection         *applied_connection = nm_device_get_applied_connection(device);
 
     tdc = _tdc_connect_new(self, iface, &connect_error);
     if (!tdc) {
@@ -299,7 +305,7 @@ master_update_slave_connection(NMDevice     *device,
 
     g_object_set(nm_connection_get_setting_connection(connection),
                  NM_SETTING_CONNECTION_MASTER,
-                 iface,
+                 nm_connection_get_uuid(applied_connection),
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_TEAM_SETTING_NAME,
                  NULL);
@@ -397,6 +403,7 @@ teamd_ready(NMDeviceTeam *self)
     NMDeviceTeamPrivate *priv   = NM_DEVICE_TEAM_GET_PRIVATE(self);
     NMDevice            *device = NM_DEVICE(self);
     gboolean             success;
+    GError              *error = NULL;
 
     if (priv->kill_in_progress) {
         /* If we are currently killing teamd, we are not
@@ -410,7 +417,11 @@ teamd_ready(NMDeviceTeam *self)
      * immediately.  But if we are, and grabbing it failed, fail the
      * device activation.
      */
-    success = ensure_teamd_connection(device);
+    success = ensure_teamd_connection(device, &error);
+    if (!success) {
+        _LOGW(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+        g_clear_error(&error);
+    }
 
     if (nm_device_get_state(device) != NM_DEVICE_STATE_PREPARE
         || priv->stage1_state != NM_DEVICE_STAGE_STATE_PENDING)
@@ -759,18 +770,20 @@ teamd_start(NMDeviceTeam *self)
 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;
+    NMDeviceTeam        *self  = NM_DEVICE_TEAM(device);
+    NMDeviceTeamPrivate *priv  = NM_DEVICE_TEAM_GET_PRIVATE(self);
+    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))
+        if (ensure_teamd_connection(device, &error))
             return NM_ACT_STAGE_RETURN_SUCCESS;
+        _LOGD(LOGD_TEAM, "could not connect to teamd: %s", error->message);
+        g_clear_error(&error);
     }
 
     s_team = nm_device_get_applied_setting(device, NM_TYPE_SETTING_TEAM);
@@ -803,6 +816,7 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
                 _LOGW(LOGD_TEAM,
                       "existing teamd config mismatch; failed to kill existing teamd: %s",
                       error->message);
+                g_clear_error(&error);
                 NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_TEAMD_CONTROL_FAILED);
                 return NM_ACT_STAGE_RETURN_FAILURE;
             }
diff --git a/src/core/devices/team/nm-device-team.h b/src/core/devices/team/nm-device-team.h
index 6f5cff93..e2a54c6e 100644
--- a/src/core/devices/team/nm-device-team.h
+++ b/src/core/devices/team/nm-device-team.h
@@ -9,7 +9,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/team/nm-team-factory.c b/src/core/devices/team/nm-team-factory.c
index b446cbd0..68ea914e 100644
--- a/src/core/devices/team/nm-team-factory.c
+++ b/src/core/devices/team/nm-team-factory.c
@@ -17,7 +17,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/tests/test-lldp.c b/src/core/devices/tests/test-lldp.c
index 048410c1..4cd602f7 100644
--- a/src/core/devices/tests/test-lldp.c
+++ b/src/core/devices/tests/test-lldp.c
@@ -12,9 +12,8 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 
+#include "libnm-lldp/nm-lldp.h"
 #include "devices/nm-lldp-listener.h"
-#include "libnm-systemd-core/nm-sd.h"
-
 #include "platform/tests/test-common.h"
 
 #include "nm-test-utils-core.h"
@@ -191,9 +190,9 @@ _test_recv_data0_check_do(GMainLoop *loop, NMLldpListener *listener, const TestR
     g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
 
     neighbor = get_lldp_neighbor(neighbors,
-                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
                                  "00:01:02:03:04:05",
-                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
                                  "1/3");
     g_assert(neighbor);
     g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 4);
@@ -547,9 +546,9 @@ _test_recv_data1_check(GMainLoop *loop, NMLldpListener *listener, TestRecvCallba
     g_assert_cmpint(g_variant_n_children(neighbors), ==, 1);
 
     neighbor = get_lldp_neighbor(neighbors,
-                                 SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
+                                 NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS,
                                  "00:01:30:F9:AD:A0",
-                                 SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
+                                 NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME,
                                  "1/1");
     g_assert(neighbor);
     g_assert_cmpint(g_variant_n_children(neighbor), ==, 1 + 4 + 16);
@@ -874,7 +873,6 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
     TestRecvCallbackInfo info = {};
     gsize                i_frames;
     GError              *error = NULL;
-    guint                sd_id;
 
     if (fixture->ifindex == 0) {
         g_test_skip("Tun device not available");
@@ -884,8 +882,7 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
     listener = nm_lldp_listener_new(fixture->ifindex, lldp_neighbors_changed, &info, &error);
     nmtst_assert_success(listener, error);
 
-    loop  = g_main_loop_new(NULL, FALSE);
-    sd_id = nm_sd_event_attach_default();
+    loop = g_main_loop_new(NULL, FALSE);
 
     for (i_frames = 0; i_frames < data->frames_len; i_frames++) {
         const TestRecvFrame *f = data->frames[i_frames];
@@ -902,7 +899,6 @@ test_recv(TestRecvFixture *fixture, gconstpointer user_data)
 
     nm_clear_pointer(&listener, nm_lldp_listener_destroy);
 
-    nm_clear_g_source(&sd_id);
     nm_clear_pointer(&loop, g_main_loop_unref);
 }
 
diff --git a/src/core/devices/wifi/nm-device-iwd-p2p.h b/src/core/devices/wifi/nm-device-iwd-p2p.h
index 9ce353c0..98cf3762 100644
--- a/src/core/devices/wifi/nm-device-iwd-p2p.h
+++ b/src/core/devices/wifi/nm-device-iwd-p2p.h
@@ -11,7 +11,7 @@
 
 #define NM_TYPE_DEVICE_IWD_P2P (nm_device_iwd_p2p_get_type())
 #define NM_DEVICE_IWD_P2P(obj) \
-    (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2P))
 #define NM_DEVICE_IWD_P2P_CLASS(klass) \
     (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_IWD_P2P, NMDeviceIwdP2PClass))
 #define NM_IS_DEVICE_IWD_P2P(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_IWD_P2P))
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index ab37cbec..e03227cd 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -8,6 +8,7 @@
 #include "nm-device-iwd.h"
 
 #include <linux/if_ether.h>
+#include <linux/rtnetlink.h>
 
 #include "devices/nm-device-private.h"
 #include "devices/nm-device.h"
@@ -32,6 +33,7 @@
 #include "supplicant/nm-supplicant-types.h"
 #include "nm-auth-utils.h"
 #include "nm-manager.h"
+#include "nm-l3-config-data.h"
 
 #define _NMLOG_DEVICE_TYPE NMDeviceIwd
 #include "devices/nm-device-logging.h"
@@ -70,6 +72,7 @@ typedef struct {
     bool                          secrets_failed : 1;
     bool                          networks_requested : 1;
     bool                          networks_changed : 1;
+    bool                          assuming : 1;
     gint64                        last_scan;
     uint32_t                      ap_id;
     guint32                       rate;
@@ -77,6 +80,16 @@ typedef struct {
     GDBusMethodInvocation        *pending_agent_request;
     NMActiveConnection           *assumed_ac;
     guint                         assumed_ac_timeout;
+
+    union {
+        struct {
+            const NML3ConfigData *pending_l3cd_6;
+            const NML3ConfigData *pending_l3cd_4;
+        };
+        const NML3ConfigData *pending_l3cd_x[2];
+    };
+
+    NMIwdManager *manager;
 } NMDeviceIwdPrivate;
 
 struct _NMDeviceIwd {
@@ -289,6 +302,7 @@ insert_ap_from_network(NMDeviceIwd *self,
                        gint64       last_seen_msec,
                        int16_t      signal)
 {
+    NMDeviceIwdPrivate             *priv          = NM_DEVICE_IWD_GET_PRIVATE(self);
     gs_unref_object GDBusProxy     *network_proxy = NULL;
     nm_auto_ref_string NMRefString *bss_path      = nm_ref_string_new(path);
     NMWifiAP                       *ap;
@@ -299,7 +313,7 @@ insert_ap_from_network(NMDeviceIwd *self,
     }
 
     network_proxy =
-        nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(), path, NM_IWD_NETWORK_INTERFACE);
+        nm_iwd_manager_get_dbus_interface(priv->manager, path, NM_IWD_NETWORK_INTERFACE);
 
     ap = ap_from_network(self, network_proxy, bss_path, last_seen_msec, signal);
     if (!ap)
@@ -525,6 +539,9 @@ cleanup_association_attempt(NMDeviceIwd *self, gboolean disconnect)
 
     if (disconnect && priv->dbus_station_proxy)
         send_disconnect(self);
+
+    nm_clear_l3cd(&priv->pending_l3cd_6);
+    nm_clear_l3cd(&priv->pending_l3cd_4);
 }
 
 static void
@@ -581,6 +598,10 @@ deactivate(NMDevice *device)
     if (!priv->dbus_obj)
         return;
 
+    /* Don't cause IWD to break the connection being assumed */
+    if (priv->assuming)
+        return;
+
     if (priv->dbus_station_proxy) {
         gs_unref_variant GVariant *value =
             g_dbus_proxy_get_cached_property(priv->dbus_station_proxy, "State");
@@ -673,7 +694,7 @@ deactivate_async(NMDevice                  *device,
 }
 
 static gboolean
-is_connection_known_network(NMConnection *connection)
+is_connection_known_network(NMIwdManager *manager, NMConnection *connection)
 {
     NMIwdNetworkSecurity security;
     gs_free char        *ssid = NULL;
@@ -681,17 +702,17 @@ is_connection_known_network(NMConnection *connection)
     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);
+    return nm_iwd_manager_is_known_network(manager, ssid, security);
 }
 
 static gboolean
-is_ap_known_network(NMWifiAP *ap)
+is_ap_known_network(NMIwdManager *manager, 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_iwd_manager_get_dbus_interface(manager,
                                           nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
                                           NM_IWD_NETWORK_INTERFACE);
     if (!network_proxy)
@@ -794,7 +815,8 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
          * thus are Known Networks.
          */
         if (security == NM_IWD_NETWORK_SECURITY_8021X) {
-            if (!is_connection_known_network(connection)) {
+            if (!is_connection_known_network(priv->manager, connection)
+                && !nm_iwd_manager_is_recently_mirrored(priv->manager, ssid)) {
                 nm_utils_error_set_literal(error,
                                            NM_UTILS_ERROR_CONNECTION_AVAILABLE_INCOMPATIBLE,
                                            "802.1x connections must have IWD provisioning files");
@@ -927,7 +949,9 @@ check_connection_available(NMDevice                      *device,
      */
     if (nm_wifi_connection_get_iwd_ssid_and_security(connection, NULL, &security)
         && security == NM_IWD_NETWORK_SECURITY_8021X) {
-        if (!is_ap_known_network(ap)) {
+        if (!is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wifi))) {
             nm_utils_error_set_literal(
                 error,
                 NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
@@ -2044,7 +2068,7 @@ assume_connection(NMDeviceIwd *self, NMWifiAP *ap)
      * 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);
+    sett_conn = nm_iwd_manager_get_ap_mirror_connection(priv->manager, ap);
     if (!sett_conn)
         goto error;
 
@@ -2217,7 +2241,8 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
      * 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))
+    if (!nm_setting_wireless_get_hidden(s_wireless)
+        || is_connection_known_network(priv->manager, connection))
         return NM_ACT_STAGE_RETURN_FAILURE;
 
 add_new:
@@ -2270,6 +2295,18 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
             goto out_fail;
         }
 
+        /* With priv->iwd_autoconnect we have to let IWD handle retries for
+         * infrastructure networks.  IWD will not necessarily retry the same
+         * network after a failure but it will likely go into an autoconnect
+         * mode and we don't want to try to override the logic.  We don't need
+         * to reset the retry count so we set no timeout.
+         */
+        if (priv->iwd_autoconnect) {
+            NMSettingsConnection *sett_conn = nm_act_request_get_settings_connection(req);
+
+            nm_settings_connection_autoconnect_retries_set(sett_conn, 0);
+        }
+
         /* 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
@@ -2310,7 +2347,9 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
          * 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)) {
+        if (nm_connection_get_setting_802_1x(connection) && !is_ap_known_network(priv->manager, ap)
+            && !nm_iwd_manager_is_recently_mirrored(priv->manager,
+                                                    nm_setting_wireless_get_ssid(s_wireless))) {
             _LOGI(LOGD_DEVICE | LOGD_WIFI,
                   "Activation: (wifi) access point '%s' has 802.1x security but is not configured "
                   "in IWD.",
@@ -2351,7 +2390,7 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
         }
 
         network_proxy = nm_iwd_manager_get_dbus_interface(
-            nm_iwd_manager_get(),
+            priv->manager,
             nm_ref_string_get_str(nm_wifi_ap_get_supplicant_path(ap)),
             NM_IWD_NETWORK_INTERFACE);
         if (!network_proxy) {
@@ -2421,6 +2460,38 @@ out_fail:
     return NM_ACT_STAGE_RETURN_FAILURE;
 }
 
+static void
+act_stage3_ip_config(NMDevice *device, int addr_family)
+{
+    NMDeviceIwd        *self    = NM_DEVICE_IWD(device);
+    NMDeviceIwdPrivate *priv    = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    if (!nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get())) {
+        NMDeviceClass *device_class = NM_DEVICE_CLASS(nm_device_iwd_parent_class);
+
+        if (device_class->act_stage3_ip_config)
+            device_class->act_stage3_ip_config(device, addr_family);
+
+        return;
+    }
+
+    if (!priv->pending_l3cd_x[IS_IPv4])
+        return;
+
+    nm_device_devip_set_state(device,
+                              addr_family,
+                              NM_DEVICE_IP_STATE_READY,
+                              priv->pending_l3cd_x[IS_IPv4]);
+    nm_clear_l3cd(&priv->pending_l3cd_x[IS_IPv4]);
+}
+
+static gboolean
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
+{
+    return !nm_iwd_manager_get_netconfig_enabled(nm_iwd_manager_get());
+}
+
 static guint32
 get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *out_force)
 {
@@ -2719,12 +2790,20 @@ state_changed(NMDeviceIwd *self, const char *new_state)
               "IWD is connecting to the wrong AP, %s activation",
               switch_ap ? "replacing" : "aborting");
         cleanup_association_attempt(self, !switch_ap);
+
+        if (!switch_ap) {
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+            return;
+        }
+
+        priv->assuming = TRUE; /* Don't send Station.Disconnect() */
         nm_device_state_changed(device,
-                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_DISCONNECTED,
                                 NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
-
-        if (switch_ap)
-            assume_connection(self, ap);
+        priv->assuming = FALSE;
+        assume_connection(self, ap);
         return;
     }
 
@@ -3101,7 +3180,7 @@ nm_device_iwd_set_dbus_object(NMDeviceIwd *self, GDBusObject *object)
         goto error;
     }
 
-    adapter_proxy = nm_iwd_manager_get_dbus_interface(nm_iwd_manager_get(),
+    adapter_proxy = nm_iwd_manager_get_dbus_interface(priv->manager,
                                                       g_variant_get_string(value, NULL),
                                                       NM_IWD_WIPHY_INTERFACE);
     if (!adapter_proxy) {
@@ -3278,7 +3357,7 @@ nm_device_iwd_agent_query(NMDeviceIwd *self, GDBusMethodInvocation *invocation)
      * it.  IWD only queries us if it has not saved the PSK (e.g. by policy)
      * or a previous attempt has failed with current secrets so it wants a
      * fresh value.  It doesn't know about agent-owned secrets so whenever
-     * possible and the PSK is saved and not asked from NM.  However if this
+     * possible, the PSK is saved and not asked from NM.  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).
@@ -3368,6 +3447,341 @@ nm_device_iwd_network_add_remove(NMDeviceIwd *self, GDBusProxy *network, bool ad
     }
 }
 
+static const NML3ConfigData *
+nm_device_iwd_parse_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice                               *device = NM_DEVICE(self);
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
+    const char                             *key;
+    GVariant                               *value;
+    NMIPConfigSource                        source       = NM_IP_CONFIG_SOURCE_UNKNOWN;
+    nm_auto_free_variant_iter GVariantIter *address_iter = NULL;
+    nm_auto_free_variant_iter GVariantIter *route_iter   = NULL;
+    nm_auto_free_variant_iter GVariantIter *dns_iter     = NULL;
+    nm_auto_free_variant_iter GVariantIter *domain_iter  = NULL;
+    NMSettingConnectionMdns                 mdns         = NM_SETTING_CONNECTION_MDNS_DEFAULT;
+    const char                             *str_value;
+    GVariantIter                           *entry_iter;
+    uint32_t                                timestamp = nm_utils_get_monotonic_timestamp_sec();
+
+    while (g_variant_iter_next(config_iter, "{&sv}", &key, &value)) {
+        _nm_unused gs_unref_variant GVariant *value_free = value;
+
+        if (nm_streq(key, "Method")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || source != NM_IP_CONFIG_SOURCE_UNKNOWN)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "static"))
+                source = NM_IP_CONFIG_SOURCE_USER;
+            else if (nm_streq(str_value, "auto"))
+                /* Use SOURCE_DHCP as shorthand for the various autoconfiguration protocols */
+                source = NM_IP_CONFIG_SOURCE_DHCP;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown Method value \"%s\"", str_value);
+        }
+
+        if (nm_streq(key, "Addresses")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || address_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &address_iter);
+        }
+
+        if (nm_streq(key, "Routes")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE("aa{sv}")) || route_iter)
+                goto param_error;
+
+            g_variant_get(value, "aa{sv}", &route_iter);
+        }
+
+        if (nm_streq(key, "DomainNameServers")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || dns_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &dns_iter);
+        }
+
+        if (nm_streq(key, "DomainNames")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY) || domain_iter)
+                goto param_error;
+
+            g_variant_get(value, "as", &domain_iter);
+        }
+
+        if (nm_streq(key, "MDNS")) {
+            if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)
+                || mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+                goto param_error;
+
+            str_value = g_variant_get_string(value, NULL);
+            if (nm_streq(str_value, "yes"))
+                mdns = NM_SETTING_CONNECTION_MDNS_YES;
+            else if (nm_streq(str_value, "no"))
+                mdns = NM_SETTING_CONNECTION_MDNS_NO;
+            else if (nm_streq(str_value, "resolve"))
+                mdns = NM_SETTING_CONNECTION_MDNS_RESOLVE;
+            else
+                _LOGW(LOGD_WIFI, "iwd_parse_netconfig: Uknown MDNS value \"%s\"", str_value);
+        }
+    }
+
+    if (!address_iter || !route_iter)
+        goto param_error;
+
+    l3cd = nm_l3_config_data_new(nm_device_get_multi_index(device),
+                                 nm_device_get_ip_ifindex(device),
+                                 source);
+
+    while (g_variant_iter_next(address_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *addr_str        = NULL;
+        NMIPAddr                                           addr_bin;
+        uint8_t                                            plen      = 0;
+        const char                                        *bcast_str = NULL;
+        NMIPAddr                                           bcast_bin = {};
+        uint32_t             valid_lifetime     = NM_PLATFORM_LIFETIME_PERMANENT;
+        uint32_t             preferred_lifetime = NM_PLATFORM_LIFETIME_PERMANENT;
+        NMPlatformIPXAddress a;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Address")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || addr_str)
+                    goto param_error;
+
+                addr_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, addr_str, &addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PrefixLength")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                plen = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "Broadcast")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || bcast_str)
+                    goto param_error;
+
+                bcast_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, bcast_str, &bcast_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "ValidLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                valid_lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "PreferredLifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || preferred_lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                preferred_lifetime = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_str == NULL)
+            goto param_error;
+
+        if (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT
+            && preferred_lifetime == NM_PLATFORM_LIFETIME_PERMANENT)
+            preferred_lifetime = valid_lifetime;
+
+        if (addr_family == AF_INET) {
+            a.a4 = (NMPlatformIP4Address){
+                .address      = addr_bin.addr4,
+                .peer_address = addr_bin.addr4,
+                .plen         = plen,
+                .timestamp    = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime     = valid_lifetime,
+                .preferred    = preferred_lifetime,
+                .addr_source  = source,
+                .use_ip4_broadcast_address = (bcast_str != NULL),
+                .broadcast_address         = bcast_bin.addr4,
+            };
+        } else {
+            a.a6 = (NMPlatformIP6Address){
+                .address     = addr_bin.addr6,
+                .plen        = 128,
+                .timestamp   = (valid_lifetime != NM_PLATFORM_LIFETIME_PERMANENT) ? timestamp : 0,
+                .lifetime    = valid_lifetime,
+                .preferred   = preferred_lifetime,
+                .addr_source = source,
+            };
+        }
+
+        nm_l3_config_data_add_address(l3cd, addr_family, NULL, &a.ax);
+    }
+
+    while (g_variant_iter_next(route_iter, "a{sv}", &entry_iter)) {
+        _nm_unused nm_auto_free_variant_iter GVariantIter *entry_iter_free = entry_iter;
+        const char                                        *dst_addr_str    = NULL;
+        NMIPAddr                                           dst_addr_bin    = {};
+        uint8_t                                            dst_plen        = 0;
+        const char                                        *router_str      = NULL;
+        NMIPAddr                                           router_bin      = {};
+        const char                                        *pref_src_str    = NULL;
+        NMIPAddr                                           pref_src_bin    = {};
+        _nm_unused uint32_t lifetime   = NM_PLATFORM_LIFETIME_PERMANENT;
+        _nm_unused uint32_t priority   = 0;
+        uint8_t             preference = 0;
+        uint32_t            mtu        = 0;
+        NMPlatformIPXRoute  r;
+
+        while (g_variant_iter_next(entry_iter, "{&sv}", &key, &value)) {
+            _nm_unused gs_unref_variant GVariant *value_free = value;
+
+            if (nm_streq(key, "Destination")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE("(sy)")) || dst_addr_str)
+                    goto param_error;
+
+                g_variant_get(value, "(&sy)", &dst_addr_str, &dst_plen);
+                if (inet_pton(addr_family, dst_addr_str, &dst_addr_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Router")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || router_str)
+                    goto param_error;
+
+                router_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, router_str, &router_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "PreferredSource")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_STRING) || pref_src_str)
+                    goto param_error;
+
+                pref_src_str = g_variant_get_string(value, NULL);
+                if (inet_pton(addr_family, pref_src_str, &pref_src_bin) != 1)
+                    goto param_error;
+            }
+
+            if (nm_streq(key, "Lifetime")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)
+                    || lifetime != NM_PLATFORM_LIFETIME_PERMANENT)
+                    goto param_error;
+
+                lifetime = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Priority")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                priority = g_variant_get_uint32(value);
+            }
+
+            if (nm_streq(key, "Preference")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTE))
+                    goto param_error;
+
+                preference = g_variant_get_byte(value);
+            }
+
+            if (nm_streq(key, "MTU")) {
+                if (!g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32))
+                    goto param_error;
+
+                mtu = g_variant_get_uint32(value);
+            }
+        }
+
+        if (addr_family == AF_INET) {
+            r.r4 = (NMPlatformIP4Route){
+                .network  = dst_addr_str ? dst_addr_bin.addr4 : 0,
+                .plen     = dst_addr_str ? dst_plen : 0,
+                .gateway  = router_str ? router_bin.addr4 : 0,
+                .pref_src = pref_src_str ? pref_src_bin.addr4 : 0,
+                .scope_inv =
+                    nm_platform_route_scope_inv(router_str ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK),
+            };
+        } else {
+            r.r6 = (NMPlatformIP6Route){
+                .network     = dst_addr_str ? dst_addr_bin.addr6 : nm_ip_addr_zero.addr6,
+                .plen        = dst_addr_str ? dst_plen : 0,
+                .gateway     = router_str ? router_bin.addr6 : nm_ip_addr_zero.addr6,
+                .pref_src    = pref_src_str ? pref_src_bin.addr6 : nm_ip_addr_zero.addr6,
+                .rt_pref     = preference,
+                .r_rtm_flags = RTNH_F_ONLINK,
+            };
+        }
+
+        r.rx.metric_any   = TRUE;
+        r.rx.mtu          = mtu;
+        r.rx.rt_source    = source;
+        r.rx.type_coerced = nm_platform_route_type_coerce(RTN_UNICAST);
+        r.rx.table_any    = TRUE;
+
+        /* TODO: set the lifetime */
+        nm_l3_config_data_add_route(l3cd, addr_family, NULL, &r.rx);
+    }
+
+    if (dns_iter) {
+        while (g_variant_iter_next(dns_iter, "&s", &str_value)) {
+            NMIPAddr dns_bin;
+
+            if (inet_pton(addr_family, str_value, &dns_bin) != 1)
+                goto param_error;
+
+            nm_l3_config_data_add_nameserver_detail(l3cd, addr_family, &dns_bin, NULL);
+            nm_l3_config_data_set_dns_priority(l3cd, addr_family, NM_DNS_PRIORITY_DEFAULT_NORMAL);
+        }
+    }
+
+    if (domain_iter) {
+        while (g_variant_iter_next(domain_iter, "&s", &str_value))
+            nm_l3_config_data_add_search(l3cd, addr_family, str_value);
+    }
+
+    if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
+        nm_l3_config_data_set_mdns(l3cd, mdns);
+
+    return nm_l3_config_data_ref_and_seal(l3cd);
+
+param_error:
+    return NULL;
+}
+
+bool
+nm_device_iwd_set_netconfig(NMDeviceIwd *self, int addr_family, GVariantIter *config_iter)
+{
+    NMDevice             *device = NM_DEVICE(self);
+    NMDeviceIwdPrivate   *priv   = NM_DEVICE_IWD_GET_PRIVATE(self);
+    const NML3ConfigData *l3cd;
+    NMDeviceState         state = nm_device_get_state(device);
+
+    if (state < NM_DEVICE_STATE_CONFIG || state > NM_DEVICE_STATE_ACTIVATED)
+        return FALSE;
+
+    l3cd = nm_device_iwd_parse_netconfig(self, addr_family, config_iter);
+    if (!l3cd) {
+        _LOGE(LOGD_WIFI, "Malformed netconfig DBus structure");
+        return FALSE;
+    }
+
+    if (state == NM_DEVICE_STATE_CONFIG) {
+        nm_l3_config_data_unref(priv->pending_l3cd_x[NM_IS_IPv4(addr_family)]);
+        priv->pending_l3cd_x[NM_IS_IPv4(addr_family)] = l3cd;
+    } else {
+        nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, l3cd);
+        nm_l3_config_data_unref(l3cd);
+    }
+
+    return TRUE;
+}
+
 static void
 autoconnect_changed(NMDevice *device, GParamSpec *pspec, NMDeviceIwd *self)
 {
@@ -3411,7 +3825,7 @@ nm_device_iwd_init(NMDeviceIwd *self)
     g_signal_connect(self, "notify::" NM_DEVICE_AUTOCONNECT, G_CALLBACK(autoconnect_changed), self);
 
     /* Make sure the manager is running */
-    (void) nm_iwd_manager_get();
+    priv->manager = g_object_ref(nm_iwd_manager_get());
 }
 
 NMDevice *
@@ -3443,6 +3857,8 @@ dispose(GObject *object)
     G_OBJECT_CLASS(nm_device_iwd_parent_class)->dispose(object);
 
     nm_assert(c_list_is_empty(&priv->aps_lst_head));
+
+    g_clear_object(&priv->manager);
 }
 
 static void
@@ -3472,12 +3888,14 @@ nm_device_iwd_class_init(NMDeviceIwdClass *klass)
     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;
+    device_class->act_stage1_prepare   = act_stage1_prepare;
+    device_class->act_stage2_config    = act_stage2_config;
+    device_class->act_stage3_ip_config = act_stage3_ip_config;
+    device_class->ready_for_ip_config  = ready_for_ip_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"
diff --git a/src/core/devices/wifi/nm-device-iwd.h b/src/core/devices/wifi/nm-device-iwd.h
index bb136bf6..8449f7f6 100644
--- a/src/core/devices/wifi/nm-device-iwd.h
+++ b/src/core/devices/wifi/nm-device-iwd.h
@@ -11,7 +11,7 @@
 #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(obj) (_NM_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))
@@ -46,4 +46,6 @@ void _nm_device_iwd_request_scan(NMDeviceIwd           *self,
 
 void nm_device_iwd_network_add_remove(NMDeviceIwd *device, GDBusProxy *network, bool add);
 
+bool nm_device_iwd_set_netconfig(NMDeviceIwd *device, int addr_family, GVariantIter *config_iter);
+
 #endif /* __NETWORKMANAGER_DEVICE_IWD_H__ */
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.h b/src/core/devices/wifi/nm-device-olpc-mesh.h
index 79b7fd5d..af3abbc0 100644
--- a/src/core/devices/wifi/nm-device-olpc-mesh.h
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.h
@@ -15,7 +15,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
index 4576af95..6c0a4d5e 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.c
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -22,7 +22,6 @@
 #include "nm-act-request.h"
 #include "nm-l3-config-data.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"
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.h b/src/core/devices/wifi/nm-device-wifi-p2p.h
index d1aadd8e..08780464 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.h
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.h
@@ -11,7 +11,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 8af974d0..171743d0 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -99,13 +99,13 @@ typedef struct {
     NMSupplMgrCreateIfaceHandle *sup_create_handle;
     NMSupplicantInterface       *sup_iface;
 
+    GSource *scan_kickoff_timeout_source;
+
     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;
@@ -508,7 +508,10 @@ _scan_notify_allowed(NMDeviceWifi *self, NMTernary do_kickoff)
         /* 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. */
+         * disrupt connected clients or peers.
+         *
+         * Explicit scans are allowed however. */
+        explicit_allowed = TRUE;
     } else if (NM_IN_SET(state, NM_DEVICE_STATE_DISCONNECTED, NM_DEVICE_STATE_FAILED)) {
         /* Can always scan when disconnected */
         explicit_allowed = TRUE;
@@ -659,7 +662,7 @@ supplicant_interface_release(NMDeviceWifi *self)
                                         NM_PENDING_ACTION_WAITING_FOR_SUPPLICANT,
                                         TRUE);
 
-    nm_clear_g_source(&priv->scan_kickoff_timeout_id);
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     nm_clear_g_source_inst(&priv->scan_request_delay_source);
     nm_clear_g_cancellable(&priv->scan_request_cancellable);
 
@@ -1718,9 +1721,9 @@ _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;
+    nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
     _scan_kickoff(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
@@ -1763,13 +1766,13 @@ _scan_kickoff(NMDeviceWifi *self)
                    / 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_kickoff_timeout_source ? ", schedule timeout" : "");
+        if (!priv->scan_kickoff_timeout_source
             && (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);
+            priv->scan_kickoff_timeout_source = nm_g_timeout_add_source(
+                priv->scan_last_request_started_at_msec + ratelimit_duration_msec - now_msec,
+                _scan_kickoff_timeout_cb,
+                self);
         }
         return;
     }
@@ -1786,14 +1789,16 @@ _scan_kickoff(NMDeviceWifi *self)
         _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);
+        nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
+        priv->scan_kickoff_timeout_source =
+            nm_g_timeout_add_source(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)");
+            _LOGT_scan(
+                "kickoff: don't scan (explicit scan requested but not allowed at the moment)");
             return;
         }
         priv->scan_explicit_requested = FALSE;
@@ -1803,7 +1808,7 @@ _scan_kickoff(NMDeviceWifi *self)
             _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);
+            nm_clear_g_source_inst(&priv->scan_kickoff_timeout_source);
             return;
         }
 
@@ -1813,12 +1818,12 @@ _scan_kickoff(NMDeviceWifi *self)
             _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);
+                       !priv->scan_kickoff_timeout_source ? ", schedule timeout" : "");
+            if (!priv->scan_kickoff_timeout_source) {
+                priv->scan_kickoff_timeout_source = nm_g_timeout_add_seconds_source(
+                    (priv->scan_periodic_next_msec - now_msec + 999) / 1000,
+                    _scan_kickoff_timeout_cb,
+                    self);
             }
             return;
         }
diff --git a/src/core/devices/wifi/nm-device-wifi.h b/src/core/devices/wifi/nm-device-wifi.h
index 3a85192e..17893a1b 100644
--- a/src/core/devices/wifi/nm-device-wifi.h
+++ b/src/core/devices/wifi/nm-device-wifi.h
@@ -10,7 +10,8 @@
 #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(obj) \
+    (_NM_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))
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 2e0d51e5..0d4daf21 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -47,13 +47,18 @@ typedef struct {
 } KnownNetworkData;
 
 typedef struct {
+    GBytes *ssid;
+    gint64  timestamp;
+} RecentlyMirroredData;
+
+typedef struct {
     NMManager          *manager;
     NMSettings         *settings;
     GCancellable       *cancellable;
     gboolean            running;
     GDBusObjectManager *object_manager;
     guint               agent_id;
-    char               *agent_path;
+    guint               netconfig_agent_id;
     GHashTable         *known_networks;
     NMDeviceIwd        *last_agent_call_device;
     char               *last_state_dir;
@@ -62,6 +67,7 @@ typedef struct {
     GHashTable         *p2p_devices;
     NMIwdWfdInfo        wfd_info;
     guint               wfd_use_count;
+    GSList             *recently_mirrored;
 } NMIwdManagerPrivate;
 
 struct _NMIwdManager {
@@ -271,6 +277,77 @@ return_error:
                                                   "Secrets not available for this connection");
 }
 
+static void
+netconfig_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);
+    gs_free char                           *name_owner = NULL;
+    const char                             *device_path;
+    gs_unref_object GDBusInterface         *device_obj = NULL;
+    NMDevice                               *device;
+    nm_auto_free_variant_iter GVariantIter *config_iter = NULL;
+    const char                             *ifname;
+    int                                     addr_family;
+
+    /* 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 (nm_streq(method_name, "ConfigureIPv4"))
+        addr_family = AF_INET;
+    else if (nm_streq(method_name, "ConfigureIPv6"))
+        addr_family = AF_INET6;
+    else
+        goto return_error;
+
+    g_variant_get(parameters, "(&oa{sv})", &device_path, &config_iter);
+
+    device_obj = g_dbus_object_manager_get_interface(priv->object_manager,
+                                                     device_path,
+                                                     NM_IWD_DEVICE_INTERFACE);
+    if (!device_obj) {
+        _LOGE("netconfig-agent-request: unable to find the device object");
+        goto return_error;
+    }
+
+    ifname = get_property_string_or_null(G_DBUS_PROXY(device_obj), "Name");
+    if (!ifname) {
+        _LOGD("Name not cached for device at %s", device_path);
+        goto return_error;
+    }
+
+    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);
+        goto return_error;
+    }
+
+    if (nm_device_iwd_set_netconfig(NM_DEVICE_IWD(device), addr_family, config_iter)) {
+        g_dbus_method_invocation_return_value(invocation, g_variant_new("()"));
+        return;
+    }
+
+    _LOGD("netconfig-agent-request: device %s did not handle the IWD Netconfig Agent request",
+          ifname);
+
+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,
+                                                  "Couldn't set netconfig data");
+}
+
 static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
     "net.connman.iwd.Agent",
     .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
@@ -296,36 +373,55 @@ static const GDBusInterfaceInfo iwd_agent_iface_info = NM_DEFINE_GDBUS_INTERFACE
                                     .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
                                         NM_DEFINE_GDBUS_ARG_INFO("reason", "s"), ), ), ), );
 
+static const GDBusInterfaceInfo iwd_netconfig_agent_iface_info =
+    NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        "net.connman.iwd.NetworkConfigurationAgent",
+        .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv4",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ),
+            NM_DEFINE_GDBUS_METHOD_INFO("ConfigureIPv6",
+                                        .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                            NM_DEFINE_GDBUS_ARG_INFO("device", "o"),
+                                            NM_DEFINE_GDBUS_ARG_INFO("config", "a{sv}"), ), ), ), );
+
 static guint
-iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_path, GError **error)
+iwd_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
 {
     static const GDBusInterfaceVTable vtable = {
         .method_call = agent_dbus_method_cb,
     };
-    char         path[50];
-    unsigned int rnd;
-    guint        id;
 
-    nm_random_get_bytes(&rnd, sizeof(rnd));
-
-    nm_sprintf_buf(path, "/agent/%u", rnd);
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
+}
 
-    id =
-        g_dbus_connection_register_object(connection,
-                                          path,
-                                          NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_agent_iface_info),
-                                          &vtable,
-                                          user_data,
-                                          NULL,
-                                          error);
+static guint
+iwd_netconfig_agent_export(GDBusConnection *connection, gpointer user_data, GError **error)
+{
+    static const GDBusInterfaceVTable vtable = {
+        .method_call = netconfig_agent_dbus_method_cb,
+    };
 
-    if (id)
-        *agent_path = g_strdup(path);
-    return id;
+    return g_dbus_connection_register_object(
+        connection,
+        NM_IWD_AGENT_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &iwd_netconfig_agent_iface_info),
+        &vtable,
+        user_data,
+        NULL,
+        error);
 }
 
 static void
-register_agent(NMIwdManager *self)
+register_agent(NMIwdManager *self, const char *method)
 {
     NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
     GDBusInterface      *agent_manager;
@@ -334,14 +430,14 @@ register_agent(NMIwdManager *self)
                                                         "/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");
+        _LOGE("unable to register the IWD Agent");
         return;
     }
 
     /* Register our agent */
     g_dbus_proxy_call(G_DBUS_PROXY(agent_manager),
-                      "RegisterAgent",
-                      g_variant_new("(o)", priv->agent_path),
+                      method,
+                      g_variant_new("(o)", NM_IWD_AGENT_PATH),
                       G_DBUS_CALL_FLAGS_NONE,
                       -1,
                       NULL,
@@ -353,6 +449,70 @@ register_agent(NMIwdManager *self)
 
 /*****************************************************************************/
 
+static void
+recently_mirrored_data_free(void *data)
+{
+    RecentlyMirroredData *rmd = data;
+
+    g_bytes_unref(rmd->ssid);
+    g_free(rmd);
+}
+
+/* When we mirror an 802.1x connection to an IWD config file, and there's an
+ * AP in range with matching SSID, that connection should become available
+ * for activation.  In IWD terms when an 802.1x network becomes a Known
+ * Network, it can be connected to using the .Connect D-Bus method.
+ *
+ * However there's a delay between writing the IWD config file and receiving
+ * the InterfaceAdded event for the Known Network so we don't immediately
+ * find out that the network can now be used.  If an NM client creates a
+ * new connection for an 802.1x AP and tries to activate it immediately,
+ * NMDeviceIWD will not allow it to because it doesn't know the network is
+ * known yet.  To work around this, we save the SSIDs of 802.1x connections
+ * we recently mirrored to IWD config files, for 2 seconds, and we treat
+ * them as Known Networks in that period since in theory activations should
+ * succeed.
+ */
+bool
+nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    gint64                now  = nm_utils_get_monotonic_timestamp_nsec();
+    GSList               *iter;
+    RecentlyMirroredData *rmd;
+
+    /* Drop entries older than 2 seconds */
+    while (priv->recently_mirrored) {
+        rmd = priv->recently_mirrored->data;
+        if (now < rmd->timestamp + 2000000000)
+            break;
+
+        priv->recently_mirrored = g_slist_remove(priv->recently_mirrored, rmd);
+        recently_mirrored_data_free(rmd);
+    }
+
+    for (iter = priv->recently_mirrored; iter; iter = iter->next) {
+        rmd = iter->data;
+        if (g_bytes_equal(ssid, rmd->ssid))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+save_mirrored(NMIwdManager *self, GBytes *ssid)
+{
+    NMIwdManagerPrivate  *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    RecentlyMirroredData *rmd  = g_malloc(sizeof(RecentlyMirroredData));
+
+    rmd->ssid               = g_bytes_ref(ssid);
+    rmd->timestamp          = nm_utils_get_monotonic_timestamp_nsec();
+    priv->recently_mirrored = g_slist_append(priv->recently_mirrored, rmd);
+}
+
+/*****************************************************************************/
+
 static KnownNetworkId *
 known_network_id_new(const char *name, NMIwdNetworkSecurity security)
 {
@@ -721,6 +881,9 @@ sett_conn_changed(NMSettingsConnection   *sett_conn,
                "iwd: changed Wi-Fi connection %s mirrored as IWD profile %s",
                nm_settings_connection_get_id(sett_conn),
                full_path);
+
+    if (security == NM_IWD_NETWORK_SECURITY_8021X)
+        save_mirrored(nm_iwd_manager_get(), ssid);
 }
 
 /* Look up an existing NMSettingsConnection for a network that has been
@@ -1283,6 +1446,7 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     gs_free_error GError           *error      = NULL;
     nm_auto_unref_keyfile GKeyFile *iwd_config = NULL;
     NMSettingsConnectionIntFlags    flags;
+    NMIwdNetworkSecurity            security;
 
     if (!nm_streq(nm_settings_connection_get_connection_type(sett_conn), "802-11-wireless"))
         return;
@@ -1338,6 +1502,12 @@ connection_added(NMSettings *settings, NMSettingsConnection *sett_conn, gpointer
     _LOGD("New Wi-Fi connection %s mirrored as IWD profile %s",
           nm_settings_connection_get_id(sett_conn),
           full_path);
+
+    if (nm_wifi_connection_get_iwd_ssid_and_security(conn, NULL, &security)
+        && security == NM_IWD_NETWORK_SECURITY_8021X) {
+        NMSettingWireless *s_wifi = nm_connection_get_setting_wireless(conn);
+        save_mirrored(nm_iwd_manager_get(), nm_setting_wireless_get_ssid(s_wifi));
+    }
 }
 
 static gboolean
@@ -1355,26 +1525,33 @@ _om_has_name_owner(GDBusObjectManager *object_manager)
 static void
 release_object_manager(NMIwdManager *self)
 {
-    NMIwdManagerPrivate *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    NMIwdManagerPrivate      *priv = NM_IWD_MANAGER_GET_PRIVATE(self);
+    GDBusConnection          *agent_connection;
+    GDBusObjectManagerClient *omc;
 
     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);
+    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.
-         */
+    /* We're 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.
+     * Just unregister the agent interfaces.  The agents are on the same
+     * object (same path) but it seems g_dbus_connection_unregister_object()
+     * should be called for each interface on the object separately.
+     */
+    if (priv->agent_id) {
         g_dbus_connection_unregister_object(agent_connection, priv->agent_id);
         priv->agent_id = 0;
-        nm_clear_g_free(&priv->agent_path);
+    }
+
+    if (priv->netconfig_agent_id) {
+        g_dbus_connection_unregister_object(agent_connection, priv->netconfig_agent_id);
+        priv->netconfig_agent_id = 0;
     }
 
     g_clear_object(&priv->object_manager);
@@ -1589,6 +1766,10 @@ next:
     }
 
     g_variant_iter_free(properties_iter);
+
+    /* Register the netconfig agent only once we know netconfig is enabled */
+    if (nm_iwd_manager_get_netconfig_enabled(self) && priv->netconfig_agent_id)
+        register_agent(self, "RegisterNetworkConfigurationAgent");
 }
 
 static void
@@ -1620,13 +1801,19 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
     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);
+    priv->agent_id = iwd_agent_export(connection, self, &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);
     }
 
+    priv->netconfig_agent_id = iwd_netconfig_agent_export(connection, self, &error);
+    if (!priv->netconfig_agent_id) {
+        _LOGE("failed to export the IWD Netconfig Agent: %s", error->message);
+        g_clear_error(&error);
+    }
+
     if (_om_has_name_owner(object_manager)) {
         GList                          *objects, *iter;
         gs_unref_object GDBusInterface *daemon = NULL;
@@ -1654,7 +1841,7 @@ got_object_manager(GObject *object, GAsyncResult *result, gpointer user_data)
         g_list_free_full(objects, g_object_unref);
 
         if (priv->agent_id)
-            register_agent(self);
+            register_agent(self, "RegisterAgent");
 
         priv->netconfig_enabled = false; /* Assume false until GetInfo() results come in */
 
@@ -1952,6 +2139,8 @@ dispose(GObject *object)
 
     g_hash_table_unref(nm_steal_pointer(&priv->p2p_devices));
 
+    g_slist_free_full(nm_steal_pointer(&priv->recently_mirrored), recently_mirrored_data_free);
+
     G_OBJECT_CLASS(nm_iwd_manager_parent_class)->dispose(object);
 }
 
diff --git a/src/core/devices/wifi/nm-iwd-manager.h b/src/core/devices/wifi/nm-iwd-manager.h
index 02cd6bba..80123c9a 100644
--- a/src/core/devices/wifi/nm-iwd-manager.h
+++ b/src/core/devices/wifi/nm-iwd-manager.h
@@ -30,8 +30,11 @@
 #define NM_IWD_P2P_SERVICE_MANAGER_INTERFACE "net.connman.iwd.p2p.ServiceManager"
 #define NM_IWD_P2P_WFD_INTERFACE             "net.connman.iwd.p2p.Display"
 
+#define NM_IWD_AGENT_PATH NM_DBUS_PATH "/iwd/agent"
+
 #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(obj) \
+    (_NM_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))
@@ -63,4 +66,6 @@ gboolean nm_iwd_manager_check_wfd_info_compatible(NMIwdManager *self, const NMIw
 gboolean nm_iwd_manager_register_wfd(NMIwdManager *self, const NMIwdWfdInfo *wfd_info);
 void     nm_iwd_manager_unregister_wfd(NMIwdManager *self);
 
+bool nm_iwd_manager_is_recently_mirrored(NMIwdManager *self, const GBytes *ssid);
+
 #endif /* __NETWORKMANAGER_IWD_MANAGER_H__ */
diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h
index c0422900..93dc8edf 100644
--- a/src/core/devices/wifi/nm-wifi-ap.h
+++ b/src/core/devices/wifi/nm-wifi-ap.h
@@ -13,7 +13,7 @@
 #include "libnm-base/nm-base.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(obj)            (_NM_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))
diff --git a/src/core/devices/wifi/nm-wifi-factory.c b/src/core/devices/wifi/nm-wifi-factory.c
index 5e354c6b..19880a90 100644
--- a/src/core/devices/wifi/nm-wifi-factory.c
+++ b/src/core/devices/wifi/nm-wifi-factory.c
@@ -24,7 +24,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wifi/nm-wifi-p2p-peer.h b/src/core/devices/wifi/nm-wifi-p2p-peer.h
index 5124d1de..b0abeb37 100644
--- a/src/core/devices/wifi/nm-wifi-p2p-peer.h
+++ b/src/core/devices/wifi/nm-wifi-p2p-peer.h
@@ -12,7 +12,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wifi/nm-wifi-utils.c b/src/core/devices/wifi/nm-wifi-utils.c
index 1a8204b0..8a8d062f 100644
--- a/src/core/devices/wifi/nm-wifi-utils.c
+++ b/src/core/devices/wifi/nm-wifi-utils.c
@@ -11,6 +11,7 @@
 #include <netinet/if_ether.h>
 #include <stdlib.h>
 
+#include "libnm-glib-aux/nm-str-buf.h"
 #include "nm-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
@@ -812,13 +813,12 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
          * setting.  Since there's so much configuration required for it, there's
          * no way it can be automatically completed.
          */
-    } else if (nm_streq0(key_mgmt, "wpa-psk")
-               || (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
-                   && (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
-                       || ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK))) {
+    } else if (ap_rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE
+               && (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_streq0(key_mgmt, "sae") ? "sae" : "wpa-psk",
                      NM_SETTING_WIRELESS_SECURITY_AUTH_ALG,
                      "open",
                      NULL);
@@ -828,7 +828,7 @@ nm_wifi_utils_complete_connection(GBytes       *ap_ssid,
                || 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", NULL);
-    } else if (ap_wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_PSK
+    } else if (nm_streq0(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,
@@ -1004,7 +1004,7 @@ psk_setting_to_iwd_config(GKeyFile *file, NMSettingWirelessSecurity *s_wsec, GEr
         if (NM_FLAGS_ANY(psk_flags, SECRETS_DONT_STORE_FLAGS)) {
             nm_log_info(
                 LOGD_WIFI,
-                "IWD network config is being created wihout the PSK but IWD will save the PSK on "
+                "IWD network config is being created without the PSK but IWD will save the PSK on "
                 "successful activation not honoring the psk-flags property");
         }
         return TRUE;
@@ -1549,10 +1549,16 @@ eap_setting_to_iwd_config(GKeyFile *file, NMSetting8021x *s_8021x, GError **erro
 }
 
 static gboolean
-ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
+ip_config_to_iwd_config(int addr_family, GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
 {
-    guint          num;
-    struct in_addr ip;
+    const int                IS_IPv4 = NM_IS_IPv4(addr_family);
+    nm_auto_str_buf NMStrBuf strbuf  = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_488, FALSE);
+    NMIPAddress             *addr;
+    guint                    num;
+    guint                    i;
+    char                     buf[NM_INET_ADDRSTRLEN + 10];
+    const char              *kf_group = IS_IPv4 ? "IPv4" : "IPv6";
+    const char              *gw;
 
     /* These settings are not acutally used unless global
      * [General].EnableNetworkConfiguration is true, which we don't support.
@@ -1564,15 +1570,26 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
     if (!s_ip)
         return TRUE;
 
+    nm_assert(NM_IS_IPv4(addr_family) ? NM_IS_SETTING_IP4_CONFIG(s_ip)
+                                      : NM_IS_SETTING_IP6_CONFIG(s_ip));
+
     num = nm_setting_ip_config_get_num_dns(s_ip);
     if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
-
+        nm_str_buf_reset(&strbuf);
         for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
+            char     sbuf[NM_INET_ADDRSTRLEN];
+            NMIPAddr a;
+
+            if (!nm_utils_dnsname_parse_assert(addr_family,
+                                               nm_setting_ip_config_get_dns(s_ip, i),
+                                               NULL,
+                                               &a,
+                                               NULL))
+                continue;
+
+            if (strbuf.len > 0)
+                nm_str_buf_append_c(&strbuf, ' ');
+            nm_str_buf_append(&strbuf, nm_inet_ntop(addr_family, &a, sbuf));
         }
         /* It doesn't matter whether we add the DNS under [IPv4] or [IPv6]
          * except that with method=auto the list will override the
@@ -1581,97 +1598,61 @@ ip4_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error
          * Note ignore-auto-dns=false isn't supported, this list always
          * overrides the DHCP DNSes.
          */
-        g_key_file_set_string(file, "IPv4", "DNS", s->str);
+        g_key_file_set_string(file, kf_group, "DNS", nm_str_buf_get_str(&strbuf));
     }
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_MANUAL))
-        return TRUE;
-
-    num = nm_setting_ip_config_get_num_addresses(s_ip);
-    if (num) {
-        NMIPAddress *addr    = nm_setting_ip_config_get_address(s_ip, 0);
-        guint        prefix  = nm_ip_address_get_prefix(addr);
-        in_addr_t    netmask = _nm_utils_ip4_prefix_to_netmask(prefix);
-        char         buf[INET_ADDRSTRLEN];
-
-        nm_ip_address_get_address_binary(addr, &ip);
-        g_key_file_set_string(file, "IPv4", "Address", nm_ip_address_get_address(addr));
-        g_key_file_set_string(file, "IPv4", "Netmask", _nm_utils_inet4_ntop(netmask, buf));
-    } else {
-        inet_pton(AF_INET, "10.42.0.100", &ip);
-        g_key_file_set_string(file, "IPv4", "Address", "10.42.0.100");
+    if (!IS_IPv4) {
+        if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
+                          NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                          NM_SETTING_IP6_CONFIG_METHOD_DHCP,
+                          NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
+            return TRUE;
+        g_key_file_set_boolean(file, kf_group, "Enabled", TRUE);
     }
 
-    if (nm_setting_ip_config_get_gateway(s_ip)) {
-        g_key_file_set_string(file, "IPv4", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
-    } else {
-        uint32_t val;
-        char     buf[INET_ADDRSTRLEN];
-
-        /* IWD won't enable static IP unless both Address and Gateway are
-         * set so generate a gateway address if not known.
-         */
-        val = (ntohl(ip.s_addr) & 0xfffffff0) + 1;
-        if (val == ntohl(ip.s_addr))
-            val += 1;
-        g_key_file_set_string(file, "IPv4", "Gateway", _nm_utils_inet4_ntop(htonl(val), buf));
-    }
-
-    return TRUE;
-}
-
-static gboolean
-ip6_config_to_iwd_config(GKeyFile *file, NMSettingIPConfig *s_ip, GError **error)
-{
-    guint        num;
-    NMIPAddress *addr;
-    char         buf[INET6_ADDRSTRLEN + 10];
-
-    if (!s_ip)
+    num = nm_setting_ip_config_get_num_addresses(s_ip);
+    if (num == 0)
         return TRUE;
 
-    num = nm_setting_ip_config_get_num_dns(s_ip);
-    if (num) {
-        nm_auto_free_gstring GString *s = g_string_sized_new(128);
-        guint                         i;
+    addr = nm_setting_ip_config_get_address(s_ip, 0);
+    gw   = nm_setting_ip_config_get_gateway(s_ip);
 
-        for (i = 0; i < num; i++) {
-            if (s->len)
-                g_string_append_c(s, ' ');
-            g_string_append(s, nm_setting_ip_config_get_dns(s_ip, i));
-        }
-        g_key_file_set_string(file, "IPv6", "DNS", s->str);
-    }
+    if (IS_IPv4) {
+        in_addr_t ip;
 
-    if (!NM_IN_STRSET(nm_setting_ip_config_get_method(s_ip),
-                      NM_SETTING_IP6_CONFIG_METHOD_AUTO,
-                      NM_SETTING_IP6_CONFIG_METHOD_DHCP,
-                      NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        nm_ip_address_get_address_binary(addr, &ip);
 
-    g_key_file_set_boolean(file, "IPv6", "Enabled", TRUE);
+        g_key_file_set_string(file, kf_group, "Address", nm_ip_address_get_address(addr));
+        g_key_file_set_string(
+            file,
+            kf_group,
+            "Netmask",
+            nm_inet4_ntop(nm_ip4_addr_netmask_from_prefix(nm_ip_address_get_prefix(addr)), buf));
 
-    if (!nm_streq0(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
-        return TRUE;
+        if (!gw) {
+            guint32 val;
 
-    if (!nm_setting_ip_config_get_num_addresses(s_ip)) {
-        g_set_error_literal(error,
-                            NM_CONNECTION_ERROR,
-                            NM_CONNECTION_ERROR_INVALID_PROPERTY,
-                            "IP address required for IPv6 manual config");
-        return FALSE;
+            /* IWD won't enable static IP unless both Address and Gateway are
+             * set so generate a gateway address if not known.
+             */
+            val = (ntohl(ip) & 0xfffffff0) + 1;
+            if (val == ntohl(ip))
+                val += 1;
+            gw = nm_inet4_ntop(htonl(val), buf);
+        }
+        g_key_file_set_string(file, kf_group, "Gateway", gw);
+    } else {
+        g_key_file_set_string(file,
+                              kf_group,
+                              "Address",
+                              nm_sprintf_buf(buf,
+                                             "%s/%u",
+                                             nm_ip_address_get_address(addr),
+                                             nm_ip_address_get_prefix(addr)));
+        if (gw)
+            g_key_file_set_string(file, kf_group, "Gateway", gw);
     }
 
-    addr = nm_setting_ip_config_get_address(s_ip, 0);
-    g_key_file_set_string(file,
-                          "IPv6",
-                          "Address",
-                          nm_sprintf_buf(buf,
-                                         "%s/%u",
-                                         nm_ip_address_get_address(addr),
-                                         nm_ip_address_get_prefix(addr)));
-    if (nm_setting_ip_config_get_gateway(s_ip))
-        g_key_file_set_string(file, "IPv6", "Gateway", nm_setting_ip_config_get_gateway(s_ip));
     return TRUE;
 }
 
@@ -1759,13 +1740,15 @@ nm_wifi_utils_connection_to_iwd_config(NMConnection *connection,
     else if (cloned_mac_addr && nm_utils_hwaddr_valid(cloned_mac_addr, ETH_ALEN))
         g_key_file_set_string(file, "Settings", "AddressOverride", cloned_mac_addr);
 
-    if (!ip4_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip4_config(connection)),
             error))
         return NULL;
 
-    if (!ip6_config_to_iwd_config(
+    if (!ip_config_to_iwd_config(
+            AF_INET6,
             file,
             NM_SETTING_IP_CONFIG(nm_connection_get_setting_ip6_config(connection)),
             error))
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
index e0209aff..b83120f9 100644
--- a/src/core/devices/wwan/nm-device-modem.c
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -273,6 +273,15 @@ modem_state_cb(NMModem *modem, int new_state_i, int old_state_i, gpointer user_d
         /* Called when the ModemManager modem enabled state is changed externally
          * to NetworkManager (eg something using MM's D-Bus API directly).
          */
+
+        if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled) {
+            /* We cannot re-enable this modem, thus device becomes unavailable. */
+            nm_device_state_changed(device,
+                                    NM_DEVICE_STATE_UNAVAILABLE,
+                                    NM_DEVICE_STATE_REASON_USER_REQUESTED);
+            return;
+        }
+
         if (nm_device_is_activating(device) || dev_state == NM_DEVICE_STATE_ACTIVATED) {
             /* user-initiated action, hence DISCONNECTED not FAILED */
             nm_device_state_changed(device,
@@ -435,6 +444,13 @@ check_connection_available(NMDevice                      *device,
         return FALSE;
     }
 
+    if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled && state <= NM_MODEM_STATE_DISABLING) {
+        nm_utils_error_set_literal(error,
+                                   NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                   "modem is disabled and NM cannot enable it");
+        return FALSE;
+    }
+
     if (state == NM_MODEM_STATE_LOCKED) {
         if (!nm_connection_get_setting_gsm(connection)) {
             nm_utils_error_set_literal(error,
@@ -599,6 +615,11 @@ set_enabled(NMDevice *device, gboolean enabled)
 
     if (enabled == FALSE) {
         nm_device_state_changed(device, NM_DEVICE_STATE_UNAVAILABLE, NM_DEVICE_STATE_REASON_NONE);
+    } else {
+        /* It's possible that the modem is enabled outside of NM. Need to recheck. */
+        nm_device_queue_recheck_available(device,
+                                          NM_DEVICE_STATE_REASON_MODEM_AVAILABLE,
+                                          NM_DEVICE_STATE_REASON_MODEM_FAILED);
     }
 }
 
@@ -617,6 +638,9 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
     if (modem_state <= NM_MODEM_STATE_INITIALIZING)
         return FALSE;
 
+    if (!NM_MODEM_GET_CLASS(priv->modem)->set_mm_enabled && modem_state <= NM_MODEM_STATE_DISABLING)
+        return FALSE;
+
     return TRUE;
 }
 
diff --git a/src/core/devices/wwan/nm-device-modem.h b/src/core/devices/wwan/nm-device-modem.h
index f171d76f..317a5bde 100644
--- a/src/core/devices/wwan/nm-device-modem.h
+++ b/src/core/devices/wwan/nm-device-modem.h
@@ -11,7 +11,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index db4fb29f..f0907c46 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -636,7 +636,7 @@ connect_context_step(NMModemBroadband *self)
         if (ctx->ip_types_i < ctx->ip_types->len) {
             NMModemIPType current;
 
-            current = g_array_index(ctx->ip_types, NMModemIPType, ctx->ip_types_i);
+            current = nm_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,
@@ -993,7 +993,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
         /* 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)) {
+            || !nm_inet_parse_bin(AF_INET, address_string, NULL, &address_network)) {
             g_set_error(&error,
                         NM_DEVICE_ERROR,
                         NM_DEVICE_ERROR_INVALID_CONNECTION,
@@ -1005,7 +1005,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 
         /* 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)) {
+        if (gw_string && !nm_inet_parse_bin(AF_INET, gw_string, NULL, &gw)) {
             g_set_error(&error,
                         NM_DEVICE_ERROR,
                         NM_DEVICE_ERROR_INVALID_CONNECTION,
@@ -1057,9 +1057,8 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 
         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_l3_config_data_add_nameserver(l3cd, AF_INET, &address_network);
+            if (nm_inet_parse_bin(AF_INET, dns[i], NULL, &address_network) && address_network > 0) {
+                nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET, &address_network, NULL);
                 _LOGI("  DNS %s", dns[i]);
             }
         }
@@ -1174,7 +1173,7 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
             struct in6_addr addr;
 
             if (inet_pton(AF_INET6, dns[i], &addr)) {
-                nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &addr);
+                nm_l3_config_data_add_nameserver_detail(l3cd, AF_INET6, &addr, NULL);
                 _LOGI("  DNS %s", dns[i]);
             }
         }
diff --git a/src/core/devices/wwan/nm-modem-broadband.h b/src/core/devices/wwan/nm-modem-broadband.h
index 627fe25a..e4467310 100644
--- a/src/core/devices/wwan/nm-modem-broadband.h
+++ b/src/core/devices/wwan/nm-modem-broadband.h
@@ -10,7 +10,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wwan/nm-modem-manager.h b/src/core/devices/wwan/nm-modem-manager.h
index e89a032b..a9339ffc 100644
--- a/src/core/devices/wwan/nm-modem-manager.h
+++ b/src/core/devices/wwan/nm-modem-manager.h
@@ -12,7 +12,7 @@
 
 #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))
+    (_NM_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))
diff --git a/src/core/devices/wwan/nm-modem-ofono.c b/src/core/devices/wwan/nm-modem-ofono.c
index fc27c3fd..b3745621 100644
--- a/src/core/devices/wwan/nm-modem-ofono.c
+++ b/src/core/devices/wwan/nm-modem-ofono.c
@@ -7,8 +7,12 @@
 
 #include "nm-modem-ofono.h"
 
+#include "libnm-glib-aux/nm-dbus-aux.h"
 #include "libnm-core-intern/nm-core-internal.h"
+#include "libnm-glib-aux/nm-uuid.h"
 #include "devices/nm-device-private.h"
+#include "nm-setting-gsm.h"
+#include "settings/nm-settings.h"
 #include "nm-modem.h"
 #include "libnm-platform/nm-platform.h"
 #include "nm-l3-config-data.h"
@@ -27,27 +31,44 @@
 /*****************************************************************************/
 
 typedef struct {
+    NMModemOfono *self;
+    char         *name;
+    char         *type;
+    gboolean      preferred;
+    GDBusProxy   *proxy;
+} OfonoContextData;
+
+typedef struct {
     GHashTable *connect_properties;
+    GHashTable *connections;
+    GHashTable *contexts;
 
     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 *connect_cancellable;
     GCancellable *sim_proxy_cancellable;
 
     GError *property_error;
 
-    char *context_path;
     char *imsi;
 
     gboolean modem_online;
+    gboolean connman_powered;
     gboolean gprs_attached;
 
     NML3ConfigData *l3cd_4;
+    NMSettings     *settings;
+
+    guint n_context_proxy_pending;
+
+    /* unowned; reference held by 'contexts' above. */
+    OfonoContextData *current_octx;
+
+    GSource *deferred_connection_timeout_source;
 } NMModemOfonoPrivate;
 
 struct _NMModemOfono {
@@ -96,6 +117,21 @@ G_DEFINE_TYPE(NMModemOfono, nm_modem_ofono, NM_TYPE_MODEM)
 
 /*****************************************************************************/
 
+/*
+ * Deterministic UUID is used to pair imsi+context with exported connection
+ * (via a pair of hash tables).
+ */
+
+static char *
+_generate_uuid(const char *imsi, const char *object_path)
+{
+    return nm_uuid_generate_from_strings(
+        NM_UUID_TYPE_VERSION5,
+        &NM_UUID_INIT(b5, 6b, ad, f5, ef, 9d, 4a, 21, a8, e7, 7d, db, 69, bb, 6b, ee),
+        imsi,
+        object_path);
+}
+
 static void
 get_capabilities(NMModem                   *_self,
                  NMDeviceModemCapabilities *modem_caps,
@@ -106,6 +142,8 @@ get_capabilities(NMModem                   *_self,
     *current_caps = NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS;
 }
 
+static void do_context_activate(NMModemOfono *self);
+
 static void
 update_modem_state(NMModemOfono *self)
 {
@@ -114,13 +152,16 @@ update_modem_state(NMModemOfono *self)
     NMModemState         new_state = NM_MODEM_STATE_DISABLED;
     const char          *reason    = NULL;
 
-    _LOGI("'Attached': %s 'Online': %s 'IMSI': %s",
+    _LOGI("'Attached': %s 'Online': %s 'Powered': %s 'IMSI': %s",
           priv->gprs_attached ? "true" : "false",
           priv->modem_online ? "true" : "false",
+          priv->connman_powered ? "true" : "false",
           priv->imsi);
 
     if (priv->modem_online == FALSE) {
         reason = "modem 'Online=false'";
+    } else if (priv->connman_powered == FALSE) {
+        reason = "ConnectionManager 'Powered=false'";
     } else if (priv->imsi == NULL && state != NM_MODEM_STATE_ENABLING) {
         reason = "modem not ready";
     } else if (priv->gprs_attached == FALSE) {
@@ -131,8 +172,27 @@ update_modem_state(NMModemOfono *self)
         reason    = "modem ready";
     }
 
-    if (state != new_state)
+    if (state != new_state) {
+        if (new_state == NM_MODEM_STATE_DISABLED && priv->deferred_connection_timeout_source) {
+            /*
+             * Do this before set_state(), because we want the final device state
+             * to be "unavailable", not "failed" or "disconnected".
+             */
+            _LOGI("canceling deferred context activation");
+
+            nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+            nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_BUSY);
+        }
+
         nm_modem_set_state(NM_MODEM(self), new_state, reason);
+
+        if (new_state == NM_MODEM_STATE_REGISTERED && priv->deferred_connection_timeout_source) {
+            _LOGI("resuming deferred context activation");
+
+            nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+            do_context_activate(self);
+        }
+    }
 }
 
 /* Disconnect */
@@ -213,7 +273,8 @@ disconnect(NMModem                   *modem,
     ctx->callback           = callback;
     ctx->callback_user_data = user_data;
 
-    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)) {
+    if (state != NM_MODEM_STATE_CONNECTED || g_cancellable_is_cancelled(cancellable)
+        || priv->current_octx == NULL) {
         nm_utils_invoke_on_idle(cancellable, disconnect_context_complete_on_idle, ctx);
         return;
     }
@@ -222,7 +283,7 @@ disconnect(NMModem                   *modem,
                        NM_MODEM_STATE_DISCONNECTING,
                        nm_modem_state_to_string(NM_MODEM_STATE_DISCONNECTING));
 
-    g_dbus_proxy_call(priv->context_proxy,
+    g_dbus_proxy_call(priv->current_octx->proxy,
                       "SetProperty",
                       g_variant_new("(sv)", "Active", g_variant_new("b", warn)),
                       G_DBUS_CALL_FLAGS_NONE,
@@ -230,6 +291,8 @@ disconnect(NMModem                   *modem,
                       ctx->cancellable,
                       disconnect_done,
                       ctx);
+
+    priv->current_octx = NULL;
 }
 
 static void
@@ -251,7 +314,7 @@ check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection,
 {
     NMModemOfono        *self = NM_MODEM_OFONO(modem);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-    const char          *id;
+    const char          *uuid;
 
     if (!_nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, NULL)) {
         nm_utils_error_set(error,
@@ -268,19 +331,12 @@ check_connection_compatible_with_modem(NMModem *modem, NMConnection *connection,
         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;
-    }
+    uuid = nm_connection_get_uuid(connection);
 
-    if (!strstr(id, priv->imsi)) {
+    if (!g_hash_table_contains(priv->contexts, uuid)) {
         nm_utils_error_set_literal(error,
                                    NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
-                                   "the connection ID does not contain the IMSI");
+                                   "connection ID does not match known contexts");
         return FALSE;
     }
 
@@ -398,11 +454,11 @@ _sim_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     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);
+    _nm_dbus_proxy_signal_connect(priv->sim_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(sim_property_changed),
+                                  self);
 
     g_dbus_proxy_call(priv->sim_proxy,
                       "GetProperties",
@@ -469,6 +525,19 @@ handle_connman_property(GDBusProxy *proxy, const char *property, GVariant *v, gp
 
             update_modem_state(self);
         }
+    } else if (nm_streq(property, "Powered") && VARIANT_IS_OF_TYPE_BOOLEAN(v)) {
+        gboolean powered     = g_variant_get_boolean(v);
+        gboolean old_powered = priv->connman_powered;
+
+        _LOGD("Powered: %s", powered ? "True" : "False");
+
+        if (old_powered != powered) {
+            priv->connman_powered = powered;
+
+            _LOGI("Powered %s -> %s", old_powered ? "true" : "false", powered ? "true" : "false");
+
+            update_modem_state(self);
+        }
     }
 }
 
@@ -484,7 +553,6 @@ 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;
@@ -498,9 +566,6 @@ connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user
         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);
@@ -526,6 +591,366 @@ connman_get_properties_done(GObject *source, GAsyncResult *result, gpointer user
 }
 
 static void
+ofono_context_data_free(OfonoContextData *octx)
+{
+    g_free(octx->name);
+    g_free(octx->type);
+
+    if (octx->proxy) {
+        g_signal_handlers_disconnect_by_data(octx->proxy, octx);
+        g_object_unref(octx->proxy);
+    }
+
+    g_slice_free(OfonoContextData, octx);
+}
+
+static void
+add_or_update_connection(NMModemOfono *self, const char *context_name, const char *uuid)
+{
+    NMModemOfonoPrivate          *priv       = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_unref_object NMConnection *connection = NULL;
+    NMSetting                    *setting;
+    NMSettingsConnection         *sett_conn;
+    gs_free_error GError         *error = NULL;
+
+    /*
+     * See first if we have an existing connection (from previous or current
+     * run of NM) that we can update in-place.
+     */
+
+    sett_conn = nm_settings_get_connection_by_uuid(priv->settings, uuid);
+    if (sett_conn
+        && !NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                         NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+        /*
+         * Either we have a coliding connection, or our connection has been
+         * modified by user. For the latter case it's safe to leave it alone;
+         * the UUID will still let it connect. However, for the first case,
+         * we can't really do anything about it unless we re-write the way we
+         * track connection <-> context, which I (Ratchanan) don't want to do
+         * right now...
+         */
+        return;
+    }
+
+    connection = nm_simple_connection_new();
+    setting    = nm_setting_connection_new();
+    g_object_set(setting,
+                 NM_SETTING_CONNECTION_ID,
+                 context_name,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NM_SETTING_CONNECTION_AUTOCONNECT,
+                 TRUE,
+                 NM_SETTING_CONNECTION_TYPE,
+                 NM_SETTING_GSM_SETTING_NAME,
+                 NULL);
+    nm_connection_add_setting(connection, setting);
+
+    setting = nm_setting_gsm_new();
+
+    /*
+     * oFono should already know how to handle placing the call, but NM
+     * insists on having a number. Pass the usual *99#.
+     */
+    g_object_set(setting, NM_SETTING_GSM_NUMBER, "*99#", NULL);
+    nm_connection_add_setting(connection, setting);
+
+    if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
+        nm_log_err(LOGD_MB,
+                   "ofono: could not not generate a connection for %s: %s",
+                   context_name,
+                   error->message);
+        return;
+    }
+
+    if (!sett_conn) {
+        nm_settings_add_connection(priv->settings,
+                                   NULL, /* plugin */
+                                   connection,
+                                   NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                                   NM_SETTINGS_CONNECTION_ADD_REASON_NONE,
+                                   NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                                   &sett_conn,
+                                   &error);
+    } else {
+        nm_settings_update_connection(priv->settings,
+                                      sett_conn,
+                                      NULL, /* plugin_name */
+                                      connection,
+                                      NM_SETTINGS_CONNECTION_PERSIST_MODE_IN_MEMORY_ONLY,
+                                      NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED,
+                                      NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                      NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE,
+                                      /* log_context_name */ "ofono",
+                                      &error);
+    }
+
+    if (!sett_conn) {
+        nm_log_warn(LOGD_MB,
+                    "ofono: could not add or update new connection for '%s' (%s): %s",
+                    context_name,
+                    uuid,
+                    error->message);
+        return;
+    }
+
+    g_hash_table_insert(priv->connections, g_strdup(uuid), g_object_ref(sett_conn));
+}
+
+/*
+ * Used when we decide a connection is not needed. Rationale being that the
+ * connection could have been modified by user since it's created by us,
+ * after which we don't want to delete, but we don't want to track it either.
+ */
+
+static void
+untrack_connection_and_delete_if_generated(NMModemOfono *self, char const *uuid)
+{
+    NMModemOfonoPrivate  *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    NMSettingsConnection *sett_conn;
+
+    sett_conn = g_hash_table_lookup(priv->connections, uuid);
+    if (!sett_conn)
+        return;
+
+    if (NM_FLAGS_HAS(nm_settings_connection_get_flags(sett_conn),
+                     NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
+        nm_settings_connection_delete(sett_conn, FALSE);
+
+    g_hash_table_remove(priv->connections, uuid);
+}
+
+static void
+update_connection_list(NMModemOfono *self)
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    GHashTableIter       iter;
+    char                *uuid;
+    OfonoContextData    *octx;
+    OfonoContextData    *octx_preferred = NULL;
+
+    _LOGI("(re-)checking which context needs a connection");
+
+    g_hash_table_iter_init(&iter, priv->contexts);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &uuid, (gpointer *) &octx)) {
+        if (octx->preferred) {
+            octx_preferred = octx;
+            break;
+        }
+    }
+
+    g_hash_table_iter_init(&iter, priv->contexts);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &uuid, (gpointer *) &octx)) {
+        gboolean connection_should_exist =
+            (!octx_preferred || octx_preferred == octx)
+            && (nm_streq(octx->type, "internet") || nm_streq(octx->type, "internet+mms"));
+        gboolean connection_exists = g_hash_table_contains(priv->connections, uuid);
+
+        if (connection_should_exist && !connection_exists) {
+            _LOGI("creating connection for %s%s",
+                  octx_preferred ? "preferred context " : "",
+                  g_dbus_proxy_get_object_path(octx->proxy));
+            add_or_update_connection(self, octx->name, uuid);
+        } else if (!connection_should_exist && connection_exists) {
+            _LOGI("removing connection for %s", g_dbus_proxy_get_object_path(octx->proxy));
+            untrack_connection_and_delete_if_generated(self, uuid);
+
+            /* priv->current_octx is deliberately not cleared here because the
+             * disconnection chain happens in another main loop iteration.
+             */
+        }
+    }
+}
+
+static void handle_settings(NMModemOfono *self, GVariant *v_dict);
+
+static void
+context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
+{
+    OfonoContextData          *octx    = user_data;
+    NMModemOfono              *self    = octx->self;
+    NMModemOfonoPrivate       *priv    = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_unref_variant GVariant *v_inner = g_variant_get_child_value(v, 0);
+
+    if (!v_inner) {
+        _LOGW("ofono: (%s): error handling PropertyChanged signal",
+              nm_modem_get_uid(NM_MODEM(self)));
+        return;
+    }
+
+    if (nm_streq(property, "Name")) {
+        gs_free char *uuid = NULL;
+
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_STRING));
+        g_free(octx->name);
+        octx->name = g_variant_dup_string(v_inner, /* &length */ NULL);
+
+        uuid = _generate_uuid(priv->imsi, g_dbus_proxy_get_object_path(proxy));
+        if (g_hash_table_contains(priv->connections, uuid))
+            add_or_update_connection(self, octx->name, uuid);
+    } else if (nm_streq(property, "Type")) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_STRING));
+        g_free(octx->type);
+        octx->type = g_variant_dup_string(v_inner, /* &length */ NULL);
+
+        update_connection_list(self);
+    } else if (nm_streq(property, "Preferred")) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_BOOLEAN));
+        octx->preferred = g_variant_get_boolean(v_inner);
+
+        update_connection_list(self);
+    } else if (nm_streq(property, "Settings") && priv->current_octx == octx) {
+        g_return_if_fail(g_variant_is_of_type(v_inner, G_VARIANT_TYPE_VARDICT));
+        handle_settings(self, v_inner);
+    }
+}
+
+static void
+_context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    OfonoContextData     *octx = user_data;
+    NMModemOfono         *self;
+    NMModemOfonoPrivate  *priv;
+    gs_free_error GError *error = NULL;
+    GDBusProxy           *proxy;
+    char                 *uuid;
+
+    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
+    if (!proxy && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+        ofono_context_data_free(octx);
+        return;
+    }
+
+    self = octx->self;
+    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    if (!proxy) {
+        _LOGW("failed to create ConnectionContext proxy: %s", error->message);
+        ofono_context_data_free(octx);
+        return;
+    }
+
+    _LOGD("recieved proxy for %s", g_dbus_proxy_get_object_path(proxy));
+    octx->proxy = proxy;
+
+    _nm_dbus_proxy_signal_connect(proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(context_property_changed),
+                                  octx);
+
+    uuid = _generate_uuid(priv->imsi, g_dbus_proxy_get_object_path(proxy));
+    g_hash_table_insert(priv->contexts, uuid, octx);
+    priv->n_context_proxy_pending--;
+
+    if (priv->n_context_proxy_pending == 0)
+        update_connection_list(self);
+}
+
+static void
+connman_context_removed(GDBusProxy *proxy, const char *object_path, gpointer user_data)
+{
+    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    gs_free char        *uuid = NULL;
+    OfonoContextData    *octx;
+
+    /* look up the connction, and if we have connection disconnect and remove it */
+    uuid = _generate_uuid(priv->imsi, object_path);
+    untrack_connection_and_delete_if_generated(self, uuid);
+
+    octx = g_hash_table_lookup(priv->contexts, uuid);
+    if (octx) {
+        gboolean preferred = octx->preferred;
+
+        if (octx == priv->current_octx)
+            priv->current_octx = NULL;
+
+        g_hash_table_remove(priv->contexts, uuid);
+
+        if (preferred)
+            update_connection_list(self);
+    }
+}
+
+static void
+connman_context_added(GDBusProxy *proxy, const char *object_path, GVariant *v, gpointer user_data)
+{
+    NMModemOfono              *self = user_data;
+    NMModemOfonoPrivate       *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    OfonoContextData          *octx;
+    gs_unref_variant GVariant *v_context_type      = NULL;
+    gs_unref_variant GVariant *v_context_name      = NULL;
+    gs_unref_variant GVariant *v_context_preferred = NULL;
+
+    nm_log_info(LOGD_MB, "ofono: processing context %s", object_path);
+
+    v_context_name = g_variant_lookup_value(v, "Name", G_VARIANT_TYPE_STRING);
+    v_context_type = g_variant_lookup_value(v, "Type", G_VARIANT_TYPE_STRING);
+    if (!v_context_name || !v_context_type) {
+        nm_log_err(LOGD_MB, "ofono: context dictionary is missing required key(s).");
+        return;
+    }
+
+    /* Preferred property exists in some oFono fork only (mostly Ubuntu Touch's).  */
+    v_context_preferred = g_variant_lookup_value(v, "Preferred", G_VARIANT_TYPE_BOOLEAN);
+
+    octx            = g_slice_new0(OfonoContextData);
+    octx->self      = self;
+    octx->name      = g_variant_dup_string(v_context_name, NULL);
+    octx->type      = g_variant_dup_string(v_context_type, NULL);
+    octx->preferred = v_context_preferred && g_variant_get_boolean(v_context_preferred);
+
+    priv->n_context_proxy_pending++;
+    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,
+                             object_path,
+                             OFONO_DBUS_INTERFACE_CONNECTION_CONTEXT,
+                             priv->connman_proxy_cancellable,
+                             _context_proxy_new_cb,
+                             octx);
+}
+
+static void
+connman_get_contexts_done(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMModemOfono              *self;
+    gs_free_error GError      *error      = NULL;
+    gs_unref_variant GVariant *v_contexts = NULL;
+    gs_unref_variant GVariant *v_objects  = NULL;
+    gs_unref_variant GVariant *v          = NULL;
+    GVariantIter               i;
+    const char                *object_path;
+
+    v_contexts = _nm_dbus_proxy_call_finish(G_DBUS_PROXY(source),
+                                            result,
+                                            G_VARIANT_TYPE("(a(oa{sv}))"),
+                                            &error);
+    if (!v_contexts && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        return;
+
+    self = user_data;
+
+    if (!v_contexts) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGW("Error getting list of contexts: %s", error->message);
+        return;
+    }
+
+    nm_log_info(LOGD_MB, "ofono: printing %s", g_variant_get_type_string(v_contexts));
+
+    v_objects = g_variant_get_child_value(v_contexts, 0);
+
+    g_variant_iter_init(&i, v_objects);
+    while (g_variant_iter_loop(&i, "(&o@a{sv})", &object_path, &v))
+        connman_context_added(NULL, object_path, v, self);
+}
+
+static void
 _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
     NMModemOfono         *self;
@@ -548,11 +973,23 @@ _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     priv->connman_proxy = proxy;
 
-    _nm_dbus_signal_connect(priv->connman_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(connman_property_changed),
-                            self);
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(connman_property_changed),
+                                  self);
+
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "ContextAdded",
+                                  G_VARIANT_TYPE("(oa{sv})"),
+                                  G_CALLBACK(connman_context_added),
+                                  self);
+
+    _nm_dbus_proxy_signal_connect(priv->connman_proxy,
+                                  "ContextRemoved",
+                                  G_VARIANT_TYPE("(o)"),
+                                  G_CALLBACK(connman_context_removed),
+                                  self);
 
     g_dbus_proxy_call(priv->connman_proxy,
                       "GetProperties",
@@ -562,6 +999,15 @@ _connman_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
                       priv->connman_proxy_cancellable,
                       connman_get_properties_done,
                       self);
+
+    g_dbus_proxy_call(priv->connman_proxy,
+                      "GetContexts",
+                      NULL,
+                      G_DBUS_CALL_FLAGS_NONE,
+                      20000,
+                      priv->connman_proxy_cancellable,
+                      connman_get_contexts_done,
+                      self);
 }
 
 static void
@@ -572,6 +1018,9 @@ handle_connman_iface(NMModemOfono *self, gboolean found)
     _LOGD("ConnectionManager interface %sfound", found ? "" : "not ");
 
     if (!found && (priv->connman_proxy || priv->connman_proxy_cancellable)) {
+        GHashTableIter        iter;
+        NMSettingsConnection *conn;
+
         _LOGI("ConnectionManager interface disappeared");
         nm_clear_g_cancellable(&priv->connman_proxy_cancellable);
         if (priv->connman_proxy) {
@@ -584,6 +1033,18 @@ handle_connman_iface(NMModemOfono *self, gboolean found)
          */
         priv->gprs_attached = FALSE;
 
+        g_hash_table_iter_init(&iter, priv->connections);
+        while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &conn)) {
+            if (NM_FLAGS_HAS(nm_settings_connection_get_flags(conn),
+                             NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED)) {
+                nm_settings_connection_delete(conn, FALSE);
+            }
+        }
+
+        priv->current_octx = NULL;
+        g_hash_table_remove_all(priv->connections);
+        g_hash_table_remove_all(priv->contexts);
+
         update_modem_state(self);
     } else if (found && (!priv->connman_proxy && !priv->connman_proxy_cancellable)) {
         _LOGI("found new ConnectionManager interface");
@@ -716,7 +1177,7 @@ stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
     self = NM_MODEM_OFONO(user_data);
     priv = NM_MODEM_OFONO_GET_PRIVATE(self);
 
-    g_clear_object(&priv->context_proxy_cancellable);
+    g_clear_object(&priv->connect_cancellable);
 
     nm_clear_pointer(&priv->connect_properties, g_hash_table_destroy);
 
@@ -730,9 +1191,8 @@ stage1_prepare_done(GObject *source, GAsyncResult *result, gpointer user_data)
 }
 
 static void
-handle_settings(GVariant *v_dict, gpointer user_data)
+handle_settings(NMModemOfono *self, GVariant *v_dict)
 {
-    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
     char                 sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
     NMPlatformIP4Address address;
@@ -744,14 +1204,20 @@ handle_settings(GVariant *v_dict, gpointer user_data)
     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 (nm_modem_get_state(NM_MODEM(self)) < NM_MODEM_STATE_REGISTERED) {
+        /*
+         * Connection definitely isn't happening. Avoid trigering bogus
+         * failure which would put device in a wrong state.
+         */
+        return;
+    }
+
     _LOGI("IPv4 static Settings:");
 
     if (!g_variant_lookup(v_dict, "Interface", "&s", &interface)) {
@@ -786,7 +1252,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Address' missing");
         goto out;
     }
-    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+    if (!s || !nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
         _LOGW("can't convert 'Address' %s to addr", s ?: "");
         goto out;
     }
@@ -801,11 +1267,11 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Netmask' missing");
         goto out;
     }
-    if (!s || !nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &address_network)) {
+    if (!s || !nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
         _LOGW("invalid 'Netmask': %s", s ?: "");
         goto out;
     }
-    address.plen = _nm_utils_ip4_netmask_to_prefix(address_network);
+    address.plen = nm_ip4_addr_netmask_to_prefix(address_network);
 
     _LOGI("Address: %s", nm_platform_ip4_address_to_string(&address, sbuf, sizeof(sbuf)));
     nm_l3_config_data_add_address_4(priv->l3cd_4, &address);
@@ -814,7 +1280,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         _LOGW("Settings 'Gateway' missing");
         goto out;
     }
-    if (!nm_utils_parse_inaddr_bin(AF_INET, s, NULL, &gateway_network)) {
+    if (!nm_inet_parse_bin(AF_INET, s, NULL, &gateway_network)) {
         _LOGW("invalid 'Gateway': %s", s);
         goto out;
     }
@@ -841,14 +1307,13 @@ handle_settings(GVariant *v_dict, gpointer user_data)
         gsize    i;
 
         for (i = 0; array[i]; i++) {
-            if (!nm_utils_parse_inaddr_bin(AF_INET, array[i], NULL, &address_network)
-                || !address_network) {
+            if (!nm_inet_parse_bin(AF_INET, array[i], NULL, &address_network) || !address_network) {
                 _LOGW("invalid NameServer: %s", array[i]);
                 continue;
             }
             any_good = TRUE;
             _LOGI("DNS: %s", array[i]);
-            nm_l3_config_data_add_nameserver(priv->l3cd_4, AF_INET, &address_network);
+            nm_l3_config_data_add_nameserver_detail(priv->l3cd_4, AF_INET, &address_network, NULL);
         }
         if (!any_good) {
             _LOGW("Settings: 'DomainNameServers': none specified");
@@ -858,7 +1323,7 @@ handle_settings(GVariant *v_dict, gpointer user_data)
 
     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)) {
+        if (s && nm_inet_parse_bin(AF_INET, s, NULL, &address_network)) {
             const NMPlatformIP4Route mms_route = {
                 .network       = address_network,
                 .plen          = 32,
@@ -893,28 +1358,6 @@ out:
 }
 
 static void
-context_property_changed(GDBusProxy *proxy, const char *property, GVariant *v, gpointer user_data)
-{
-    NMModemOfono              *self   = NM_MODEM_OFONO(user_data);
-    gs_unref_variant GVariant *v_dict = NULL;
-
-    _LOGD("PropertyChanged: %s", property);
-
-    if (g_strcmp0(property, "Settings") != 0)
-        return;
-
-    v_dict = g_variant_get_child_value(v, 0);
-    if (!v_dict) {
-        _LOGW("ofono: (%s): error getting IPv4 Settings", nm_modem_get_uid(NM_MODEM(self)));
-        return;
-    }
-
-    g_assert(g_variant_is_of_type(v_dict, G_VARIANT_TYPE_VARDICT));
-
-    handle_settings(v_dict, user_data);
-}
-
-static void
 stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_method)
 {
     NMModemOfono         *self  = NM_MODEM_OFONO(modem);
@@ -948,17 +1391,13 @@ out:
 static void
 context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
 {
-    NMModemOfono              *self;
-    NMModemOfonoPrivate       *priv;
+    NMModemOfono              *self       = user_data;
     gs_free_error GError      *error      = NULL;
     gs_unref_variant GVariant *properties = NULL;
     gs_unref_variant GVariant *settings   = NULL;
     gs_unref_variant GVariant *v_dict     = NULL;
     gboolean                   active;
 
-    self = NM_MODEM_OFONO(user_data);
-    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-
     properties = g_dbus_proxy_call_finish(proxy, result, &error);
 
     if (!properties) {
@@ -978,13 +1417,6 @@ context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_dat
         goto error;
     }
 
-    /* Watch for custom ofono PropertyChanged signals */
-    _nm_dbus_signal_connect(priv->context_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(context_property_changed),
-                            self);
-
     if (active) {
         _LOGD("ofono: connection is already Active");
 
@@ -994,9 +1426,9 @@ context_properties_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_dat
             goto error;
         }
 
-        handle_settings(settings, user_data);
+        handle_settings(self, settings);
     } else {
-        g_dbus_proxy_call(priv->context_proxy,
+        g_dbus_proxy_call(proxy,
                           "SetProperty",
                           g_variant_new("(sv)", "Active", g_variant_new("b", TRUE)),
                           G_DBUS_CALL_FLAGS_NONE,
@@ -1012,36 +1444,15 @@ error:
 }
 
 static void
-context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+do_context_activate(NMModemOfono *self)
 {
-    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;
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
 
-    self = NM_MODEM_OFONO(user_data);
-    priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+    g_return_if_fail(NM_IS_MODEM_OFONO(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;
-    }
+    nm_clear_g_cancellable(&priv->connect_cancellable);
 
-    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;
-    }
+    priv->connect_cancellable = g_cancellable_new();
 
     /* 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
@@ -1051,39 +1462,16 @@ context_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     /* We need to directly query ConnectionContextinteface to get the current
      * property values */
-    g_dbus_proxy_call(priv->context_proxy,
+    g_dbus_proxy_call(priv->current_octx->proxy,
                       "GetProperties",
                       NULL,
                       G_DBUS_CALL_FLAGS_NONE,
                       20000,
-                      NULL,
+                      priv->connect_cancellable,
                       (GAsyncReadyCallback) context_properties_cb,
                       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)
 {
@@ -1109,6 +1497,20 @@ create_connect_properties(NMConnection *connection)
     return properties;
 }
 
+static gboolean
+wait_for_signal_timeout(gpointer user_data)
+{
+    NMModemOfono        *self = NM_MODEM_OFONO(user_data);
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    nm_modem_emit_prepare_result(NM_MODEM(self),
+                                 FALSE,
+                                 NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT);
+
+    nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+    return G_SOURCE_REMOVE;
+}
+
 static NMActStageReturn
 modem_act_stage1_prepare(NMModem             *modem,
                          NMConnection        *connection,
@@ -1116,20 +1518,10 @@ modem_act_stage1_prepare(NMModem             *modem,
 {
     NMModemOfono        *self = NM_MODEM_OFONO(modem);
     NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
-    const char          *context_id;
-    char               **id = NULL;
+    const char          *uuid = nm_connection_get_uuid(connection);
+    OfonoContextData    *octx = g_hash_table_lookup(priv->contexts, uuid);
 
-    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) {
+    if (!octx) {
         NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
         return NM_ACT_STAGE_RETURN_FAILURE;
     }
@@ -1139,11 +1531,19 @@ modem_act_stage1_prepare(NMModem             *modem,
 
     priv->connect_properties = create_connect_properties(connection);
 
-    _LOGI("activating context %s", priv->context_path);
+    _LOGI("activating context %s", g_dbus_proxy_get_object_path(octx->proxy));
+    priv->current_octx = octx;
 
     update_modem_state(self);
     if (nm_modem_get_state(modem) == NM_MODEM_STATE_REGISTERED) {
         do_context_activate(self);
+    } else if (nm_modem_get_state(modem) == NM_MODEM_STATE_SEARCHING) {
+        _LOGI("activation deferred while modem is searching for signal.");
+
+        nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+        priv->deferred_connection_timeout_source =
+            nm_g_timeout_add_seconds_source(60 /* seconds */, wait_for_signal_timeout, self);
+        /* update_modem_state() will advance the next step. */
     } else {
         _LOGW("could not activate context: modem is not registered.");
         NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER);
@@ -1176,11 +1576,11 @@ modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
     priv->modem_proxy = proxy;
 
-    _nm_dbus_signal_connect(priv->modem_proxy,
-                            "PropertyChanged",
-                            G_VARIANT_TYPE("(sv)"),
-                            G_CALLBACK(modem_property_changed),
-                            self);
+    _nm_dbus_proxy_signal_connect(priv->modem_proxy,
+                                  "PropertyChanged",
+                                  G_VARIANT_TYPE("(sv)"),
+                                  G_CALLBACK(modem_property_changed),
+                                  self);
 
     g_dbus_proxy_call(priv->modem_proxy,
                       "GetProperties",
@@ -1196,7 +1596,17 @@ modem_proxy_new_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 
 static void
 nm_modem_ofono_init(NMModemOfono *self)
-{}
+{
+    NMModemOfonoPrivate *priv = NM_MODEM_OFONO_GET_PRIVATE(self);
+
+    priv->modem_proxy_cancellable = g_cancellable_new();
+    priv->connections = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_object_unref);
+    priv->contexts    = g_hash_table_new_full(nm_str_hash,
+                                           g_str_equal,
+                                           g_free,
+                                           (GDestroyNotify) ofono_context_data_free);
+    priv->settings    = g_object_ref(NM_SETTINGS_GET);
+}
 
 static void
 constructed(GObject *object)
@@ -1204,8 +1614,6 @@ 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,
@@ -1257,7 +1665,7 @@ dispose(GObject *object)
 
     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->connect_cancellable);
     nm_clear_g_cancellable(&priv->sim_proxy_cancellable);
 
     if (priv->connect_properties) {
@@ -1265,6 +1673,16 @@ dispose(GObject *object)
         priv->connect_properties = NULL;
     }
 
+    if (priv->connections) {
+        g_hash_table_destroy(priv->connections);
+        priv->connections = NULL;
+    }
+
+    if (priv->contexts) {
+        g_hash_table_destroy(priv->contexts);
+        priv->contexts = NULL;
+    }
+
     nm_clear_l3cd(&priv->l3cd_4);
 
     if (priv->modem_proxy) {
@@ -1277,19 +1695,21 @@ dispose(GObject *object)
         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);
     }
 
+    if (priv->settings) {
+        g_signal_handlers_disconnect_by_data(priv->settings, self);
+        g_clear_object(&priv->settings);
+    }
+
     g_free(priv->imsi);
     priv->imsi = NULL;
 
+    nm_clear_g_source_inst(&priv->deferred_connection_timeout_source);
+
     G_OBJECT_CLASS(nm_modem_ofono_parent_class)->dispose(object);
 }
 
diff --git a/src/core/devices/wwan/nm-modem-ofono.h b/src/core/devices/wwan/nm-modem-ofono.h
index ebe21023..02ba49ad 100644
--- a/src/core/devices/wwan/nm-modem-ofono.h
+++ b/src/core/devices/wwan/nm-modem-ofono.h
@@ -8,8 +8,9 @@
 
 #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_TYPE_MODEM_OFONO (nm_modem_ofono_get_type())
+#define NM_MODEM_OFONO(obj) \
+    (_NM_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))
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
index a5ee8ce8..0159d351 100644
--- a/src/core/devices/wwan/nm-modem.c
+++ b/src/core/devices/wwan/nm-modem.c
@@ -353,6 +353,12 @@ nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
     NMModemPrivate *priv       = NM_MODEM_GET_PRIVATE(self);
     NMModemState    prev_state = priv->state;
 
+    /* Not all modem classes support set_mm_enabled */
+    if (!NM_MODEM_GET_CLASS(self)->set_mm_enabled) {
+        _LOGD("cannot enable modem: not implemented");
+        return;
+    }
+
     if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) {
         _LOGD("cannot enable modem: already enabled");
         return;
@@ -375,9 +381,7 @@ nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
         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);
+    NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);
 
     /* Pre-empt the state change signal */
     nm_modem_set_state(self,
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
index ca541073..ec001102 100644
--- a/src/core/devices/wwan/nm-modem.h
+++ b/src/core/devices/wwan/nm-modem.h
@@ -11,7 +11,7 @@
 #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(obj)            (_NM_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))
diff --git a/src/core/devices/wwan/nm-wwan-factory.c b/src/core/devices/wwan/nm-wwan-factory.c
index 169407c7..1c6d351f 100644
--- a/src/core/devices/wwan/nm-wwan-factory.c
+++ b/src/core/devices/wwan/nm-wwan-factory.c
@@ -18,7 +18,7 @@
 
 #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))
+    (_NM_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))