about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c171
-rw-r--r--src/core/NetworkManagerUtils.h34
-rw-r--r--src/core/devices/adsl/nm-device-adsl.c7
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.c7
-rw-r--r--src/core/devices/nm-device-6lowpan.c14
-rw-r--r--src/core/devices/nm-device-bond.c62
-rw-r--r--src/core/devices/nm-device-bridge.c235
-rw-r--r--src/core/devices/nm-device-ethernet.c7
-rw-r--r--src/core/devices/nm-device-factory.h3
-rw-r--r--src/core/devices/nm-device-generic.c46
-rw-r--r--src/core/devices/nm-device-infiniband.c11
-rw-r--r--src/core/devices/nm-device-ip-tunnel.c9
-rw-r--r--src/core/devices/nm-device-logging.h52
-rw-r--r--src/core/devices/nm-device-macvlan.c23
-rw-r--r--src/core/devices/nm-device-ppp.c7
-rw-r--r--src/core/devices/nm-device-private.h6
-rw-r--r--src/core/devices/nm-device-tun.c9
-rw-r--r--src/core/devices/nm-device-vlan.c9
-rw-r--r--src/core/devices/nm-device-vrf.c20
-rw-r--r--src/core/devices/nm-device-vxlan.c9
-rw-r--r--src/core/devices/nm-device-wpan.c7
-rw-r--r--src/core/devices/nm-device.c818
-rw-r--r--src/core/devices/nm-device.h43
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c13
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.c13
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c58
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c6
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c335
-rw-r--r--src/core/devices/team/nm-device-team.c11
-rw-r--r--src/core/devices/wifi/nm-device-iwd-p2p.c7
-rw-r--r--src/core/devices/wifi/nm-device-iwd.c36
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.c2
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c7
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c15
-rw-r--r--src/core/devices/wwan/nm-device-modem.c7
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c83
-rw-r--r--src/core/devices/wwan/nm-modem.c31
-rw-r--r--src/core/devices/wwan/nm-modem.h2
-rw-r--r--src/core/dhcp/nm-dhcp-client-logging.h7
-rw-r--r--src/core/dhcp/nm-dhcp-client.c10
-rw-r--r--src/core/dhcp/nm-dhcp-client.h9
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c12
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c31
-rw-r--r--src/core/dhcp/nm-dhcp-options.c2
-rw-r--r--src/core/dhcp/nm-dhcp-options.h3
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c29
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c1
-rw-r--r--src/core/dns/nm-dns-manager.c90
-rw-r--r--src/core/main.c16
-rw-r--r--src/core/nm-act-request.c4
-rw-r--r--src/core/nm-active-connection.c2
-rw-r--r--src/core/nm-auth-utils.c2
-rw-r--r--src/core/nm-bond-manager.c8
-rw-r--r--src/core/nm-checkpoint.c39
-rw-r--r--src/core/nm-config-data.c188
-rw-r--r--src/core/nm-config-data.h47
-rw-r--r--src/core/nm-config.c6
-rw-r--r--src/core/nm-config.h2
-rw-r--r--src/core/nm-connectivity.c18
-rw-r--r--src/core/nm-core-utils.c350
-rw-r--r--src/core/nm-core-utils.h45
-rw-r--r--src/core/nm-dbus-manager.c81
-rw-r--r--src/core/nm-dbus-manager.h4
-rw-r--r--src/core/nm-firewall-utils.c51
-rw-r--r--src/core/nm-firewall-utils.h3
-rw-r--r--src/core/nm-keep-alive.c2
-rw-r--r--src/core/nm-l3-config-data.c29
-rw-r--r--src/core/nm-l3-config-data.h2
-rw-r--r--src/core/nm-l3-ipv6ll.c12
-rw-r--r--src/core/nm-l3cfg.c757
-rw-r--r--src/core/nm-l3cfg.h8
-rw-r--r--src/core/nm-manager.c916
-rw-r--r--src/core/nm-manager.h47
-rw-r--r--src/core/nm-netns.c597
-rw-r--r--src/core/nm-netns.h42
-rw-r--r--src/core/nm-policy.c533
-rw-r--r--src/core/nm-policy.h2
-rw-r--r--src/core/nm-test-utils-core.h4
-rw-r--r--src/core/platform/nm-fake-platform.c99
-rw-r--r--src/core/platform/tests/test-cleanup.c4
-rw-r--r--src/core/platform/tests/test-common.c6
-rw-r--r--src/core/platform/tests/test-link.c252
-rw-r--r--src/core/platform/tests/test-route.c9
-rw-r--r--src/core/settings/nm-secret-agent.c2
-rw-r--r--src/core/settings/nm-settings-connection.c321
-rw-r--r--src/core/settings/nm-settings-connection.h59
-rw-r--r--src/core/settings/nm-settings.c137
-rw-r--r--src/core/settings/nm-settings.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.c16
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c28
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c1
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c26
-rw-r--r--src/core/settings/plugins/ifcfg-rh/shvar.c23
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-plugin.c60
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-writer.c31
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-writer.h18
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c16
-rw-r--r--src/core/supplicant/nm-supplicant-manager.c6
-rw-r--r--src/core/tests/test-core-with-expect.c10
-rw-r--r--src/core/tests/test-core.c147
101 files changed, 5332 insertions, 2189 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index 6f4c60f8..8606082c 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -23,6 +23,7 @@
 #include "nm-setting-connection.h"
 #include "nm-setting-ip4-config.h"
 #include "nm-setting-ip6-config.h"
+#include "settings/nm-settings.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-platform/nmp-object.h"
 
@@ -30,6 +31,7 @@
 #include "libnm-platform/nm-linux-platform.h"
 #include "libnm-platform/nm-platform-utils.h"
 #include "nm-auth-utils.h"
+#include "devices/nm-device.h"
 
 /*****************************************************************************/
 
@@ -684,6 +686,53 @@ check_connection_cloned_mac_address(NMConnection *orig,
 }
 
 static gboolean
+check_connection_controller(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
+{
+    GHashTable           *props;
+    const char           *orig_controller = NULL, *cand_controller = NULL;
+    NMSettingConnection  *s_con_orig, *s_con_cand, *s_con_controller;
+    NMSettingsConnection *con_controller;
+
+    props = check_property_in_hash(settings,
+                                   NM_SETTING_CONNECTION_SETTING_NAME,
+                                   NM_SETTING_CONNECTION_MASTER);
+    if (!props)
+        return TRUE;
+
+    s_con_orig      = nm_connection_get_setting_connection(orig);
+    s_con_cand      = nm_connection_get_setting_connection(candidate);
+    orig_controller = nm_setting_connection_get_master(s_con_orig);
+    cand_controller = nm_setting_connection_get_master(s_con_cand);
+
+    /* A generated connection uses the UUID to specify the controller. Accept
+     * candidates that specify as controller an interface name matching that
+     * UUID */
+    if (orig_controller && cand_controller) {
+        if (nm_utils_is_uuid(orig_controller)) {
+            con_controller = nm_settings_get_connection_by_uuid(NM_SETTINGS_GET, orig_controller);
+            /* no connection found for that uuid */
+            if (!con_controller)
+                return FALSE;
+
+            s_con_controller =
+                nm_settings_connection_get_setting(con_controller, NM_META_SETTING_TYPE_CONNECTION);
+            if (nm_streq0(nm_setting_connection_get_interface_name(s_con_controller),
+                          cand_controller)) {
+                remove_from_hash(settings,
+                                 props,
+                                 NM_SETTING_CONNECTION_SETTING_NAME,
+                                 NM_SETTING_CONNECTION_MASTER);
+                return TRUE;
+            } else {
+                return FALSE;
+            }
+        }
+    }
+
+    return FALSE;
+}
+
+static gboolean
 check_connection_s390_props(NMConnection *orig, NMConnection *candidate, GHashTable *settings)
 {
     GHashTable     *props1, *props2, *props3;
@@ -764,9 +813,16 @@ check_possible_match(NMConnection *orig,
     if (!check_connection_cloned_mac_address(orig, candidate, settings))
         return NULL;
 
+    if (!check_connection_controller(orig, candidate, settings))
+        return NULL;
+
     if (!check_connection_s390_props(orig, candidate, settings))
         return NULL;
 
+    /* match properties are for matching from static to generated connections,
+     * so they are not really part of the difference. */
+    g_hash_table_remove(settings, NM_SETTING_MATCH_SETTING_NAME);
+
     if (g_hash_table_size(settings) == 0)
         return candidate;
     else
@@ -896,6 +952,73 @@ nm_utils_match_connection(NMConnection *const   *connections,
 
 /*****************************************************************************/
 
+const struct _NMMatchSpecDeviceData *
+nm_match_spec_device_data_init_from_device(struct _NMMatchSpecDeviceData *out_data,
+                                           NMDevice                      *device)
+{
+    const char *hw_address;
+    gboolean    is_fake;
+
+    nm_assert(out_data);
+
+    if (!device) {
+        *out_data = (NMMatchSpecDeviceData){};
+        return out_data;
+    }
+
+    nm_assert(NM_IS_DEVICE(device));
+
+    hw_address = nm_device_get_permanent_hw_address_full(
+        device,
+        !nm_device_get_unmanaged_flags(device, NM_UNMANAGED_PLATFORM_INIT),
+        &is_fake);
+
+    /* Note that here we access various getters on @device, without cloning
+     * or taking ownership and return it to the caller.
+     *
+     * The returned data is only valid, until NMDevice gets modified again. */
+
+    *out_data = (NMMatchSpecDeviceData){
+        .interface_name   = nm_device_get_iface(device),
+        .device_type      = nm_device_get_type_description(device),
+        .driver           = nm_device_get_driver(device),
+        .driver_version   = nm_device_get_driver_version(device),
+        .hwaddr           = is_fake ? NULL : hw_address,
+        .s390_subchannels = nm_device_get_s390_subchannels(device),
+        .dhcp_plugin      = nm_dhcp_manager_get_config(nm_dhcp_manager_get()),
+    };
+
+    return out_data;
+}
+
+const NMMatchSpecDeviceData *
+nm_match_spec_device_data_init_from_platform(NMMatchSpecDeviceData *out_data,
+                                             const NMPlatformLink  *pllink,
+                                             const char            *match_device_type,
+                                             const char            *match_dhcp_plugin)
+{
+    nm_assert(out_data);
+
+    /* we can only match by certain properties that are available on the
+     * platform link (and even @pllink might be missing.
+     *
+     * It's still useful because of specs like "*" and "except:interface-name:eth0",
+     * which match even in that case. */
+
+    *out_data = (NMMatchSpecDeviceData){
+        .interface_name   = pllink ? pllink->name : NULL,
+        .device_type      = match_device_type,
+        .driver           = pllink ? pllink->driver : NULL,
+        .driver_version   = NULL,
+        .hwaddr           = NULL,
+        .s390_subchannels = NULL,
+        .dhcp_plugin      = match_dhcp_plugin,
+    };
+    return out_data;
+}
+
+/*****************************************************************************/
+
 int
 nm_match_spec_device_by_pllink(const NMPlatformLink *pllink,
                                const char           *match_device_type,
@@ -903,32 +1026,15 @@ nm_match_spec_device_by_pllink(const NMPlatformLink *pllink,
                                const GSList         *specs,
                                int                   no_match_value)
 {
-    NMMatchSpecMatchType m;
+    NMMatchSpecMatchType  m;
+    NMMatchSpecDeviceData data;
 
-    /* we can only match by certain properties that are available on the
-     * platform link (and even @pllink might be missing.
-     *
-     * It's still useful because of specs like "*" and "except:interface-name:eth0",
-     * which match even in that case. */
     m = nm_match_spec_device(specs,
-                             pllink ? pllink->name : NULL,
-                             match_device_type,
-                             pllink ? pllink->driver : NULL,
-                             NULL,
-                             NULL,
-                             NULL,
-                             match_dhcp_plugin);
-
-    switch (m) {
-    case NM_MATCH_SPEC_MATCH:
-        return TRUE;
-    case NM_MATCH_SPEC_NEG_MATCH:
-        return FALSE;
-    case NM_MATCH_SPEC_NO_MATCH:
-        return no_match_value;
-    }
-    nm_assert_not_reached();
-    return no_match_value;
+                             nm_match_spec_device_data_init_from_platform(&data,
+                                                                          pllink,
+                                                                          match_device_type,
+                                                                          match_dhcp_plugin));
+    return nm_match_spec_match_type_to_bool(m, no_match_value);
 }
 
 /*****************************************************************************/
@@ -1748,6 +1854,13 @@ nm_utils_platform_capture_ip_setting(NMPlatform *platform,
             method = maybe_ipv6_disabled ? NM_SETTING_IP6_CONFIG_METHOD_DISABLED
                                          : NM_SETTING_IP6_CONFIG_METHOD_IGNORE;
     }
+
+    /* The IPv6 method "ignore" and "disabled" are not supported for loopback */
+    if (ifindex == 1
+        && NM_IN_STRSET(method,
+                        NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
+                        NM_SETTING_IP6_CONFIG_METHOD_IGNORE))
+        method = NM_SETTING_IP6_CONFIG_METHOD_AUTO;
     g_object_set(s_ip, NM_SETTING_IP_CONFIG_METHOD, method, NULL);
 
     nmp_lookup_init_object_by_ifindex(&lookup, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4), ifindex);
@@ -1860,3 +1973,13 @@ nm_linux_platform_setup_with_tc_cache(void)
 {
     nm_platform_setup(nm_linux_platform_new(NULL, FALSE, FALSE, TRUE));
 }
+
+/*****************************************************************************/
+
+NM_UTILS_FLAGS2STR_DEFINE(
+    nm_settings_autoconnect_blocked_reason_to_string,
+    NMSettingsAutoconnectBlockedReason,
+    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE, "none"),
+    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST, "user-request"),
+    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED, "failed"),
+    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS, "no-secrets"), );
diff --git a/src/core/NetworkManagerUtils.h b/src/core/NetworkManagerUtils.h
index 67c9cba4..7d8afe5a 100644
--- a/src/core/NetworkManagerUtils.h
+++ b/src/core/NetworkManagerUtils.h
@@ -89,6 +89,20 @@ NMConnection *nm_utils_match_connection(NMConnection *const   *connections,
                                         NMUtilsMatchFilterFunc match_filter_func,
                                         gpointer               match_filter_data);
 
+/*****************************************************************************/
+
+struct _NMMatchSpecDeviceData;
+
+const struct _NMMatchSpecDeviceData *
+nm_match_spec_device_data_init_from_device(struct _NMMatchSpecDeviceData *out_data,
+                                           NMDevice                      *device);
+
+const struct _NMMatchSpecDeviceData *
+nm_match_spec_device_data_init_from_platform(struct _NMMatchSpecDeviceData *out_data,
+                                             const NMPlatformLink          *pllink,
+                                             const char                    *match_device_type,
+                                             const char                    *match_dhcp_plugin);
+
 int nm_match_spec_device_by_pllink(const NMPlatformLink *pllink,
                                    const char           *match_device_type,
                                    const char           *match_dhcp_plugin,
@@ -228,6 +242,26 @@ void nm_utils_ip_routes_to_dbus(int                          addr_family,
 
 /*****************************************************************************/
 
+typedef enum _nm_packed {
+    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE = 0,
+
+    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST = (1LL << 0),
+    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED       = (1LL << 1),
+    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS   = (1LL << 2),
+
+    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_ALL =
+        (NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST
+         | NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED
+         | NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS),
+} NMSettingsAutoconnectBlockedReason;
+
+const char *
+nm_settings_autoconnect_blocked_reason_to_string(NMSettingsAutoconnectBlockedReason reason,
+                                                 char                              *buf,
+                                                 gsize                              len);
+
+/*****************************************************************************/
+
 /* For now, all we track about a DHCP lease is the GHashTable with
  * the options.
  *
diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c
index fcd16b1c..89bc84d8 100644
--- a/src/core/devices/adsl/nm-device-adsl.c
+++ b/src/core/devices/adsl/nm-device-adsl.c
@@ -67,13 +67,16 @@ get_generic_capabilities(NMDevice *dev)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingAdsl *s_adsl;
     const char    *protocol;
 
     if (!NM_DEVICE_CLASS(nm_device_adsl_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_adsl = nm_connection_get_setting_adsl(connection);
diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c
index a8258123..8b13e97d 100644
--- a/src/core/devices/bluetooth/nm-device-bt.c
+++ b/src/core/devices/bluetooth/nm-device-bt.c
@@ -192,7 +192,10 @@ can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **speci
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceBt         *self = NM_DEVICE_BT(device);
     NMDeviceBtPrivate  *priv = NM_DEVICE_BT_GET_PRIVATE(self);
@@ -200,7 +203,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     const char         *bdaddr;
 
     if (!NM_DEVICE_CLASS(nm_device_bt_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     if (!get_connection_bt_type_check(self, connection, NULL, error))
diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c
index 870a1c14..f3386ddb 100644
--- a/src/core/devices/nm-device-6lowpan.c
+++ b/src/core/devices/nm-device-6lowpan.c
@@ -72,14 +72,22 @@ create_and_realize(NMDevice              *device,
     s_6lowpan = NM_SETTING_6LOWPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_6LOWPAN));
     g_return_val_if_fail(s_6lowpan, FALSE);
 
-    parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0;
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "6LoWPAN device can not be created without a parent interface");
+        return FALSE;
+    }
 
+    parent_ifindex = nm_device_get_ifindex(parent);
     if (parent_ifindex <= 0) {
         g_set_error(error,
                     NM_DEVICE_ERROR,
                     NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
-                    "6LoWPAN devices can not be created without a parent interface");
-        g_return_val_if_fail(!parent, FALSE);
+                    "cannot retrieve ifindex of interface %s (%s)",
+                    nm_device_get_iface(parent),
+                    nm_device_get_type_desc(parent));
         return FALSE;
     }
 
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 56c5ec50..10fe8092 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -39,7 +39,8 @@
         NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \
         NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_TLB_DYNAMIC_LB,         \
         NM_SETTING_BOND_OPTION_USE_CARRIER, NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY,       \
-        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP, NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY
+        NM_SETTING_BOND_OPTION_NUM_GRAT_ARP, NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY,      \
+        NM_SETTING_BOND_OPTION_ARP_MISSED_MAX, NM_SETTING_BOND_OPTION_LACP_ACTIVE
 
 #define OPTIONS_REAPPLY_SUBSET                                                             \
     NM_SETTING_BOND_OPTION_MIIMON, NM_SETTING_BOND_OPTION_UPDELAY,                         \
@@ -51,11 +52,12 @@
         NM_SETTING_BOND_OPTION_PACKETS_PER_SLAVE, NM_SETTING_BOND_OPTION_PRIMARY_RESELECT, \
         NM_SETTING_BOND_OPTION_RESEND_IGMP, NM_SETTING_BOND_OPTION_USE_CARRIER,            \
         NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY, NM_SETTING_BOND_OPTION_NUM_GRAT_ARP,      \
-        NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY
+        NM_SETTING_BOND_OPTION_PEER_NOTIF_DELAY, NM_SETTING_BOND_OPTION_ARP_MISSED_MAX,    \
+        NM_SETTING_BOND_OPTION_LACP_ACTIVE
 
 #define OPTIONS_REAPPLY_FULL                                     \
     OPTIONS_REAPPLY_SUBSET, NM_SETTING_BOND_OPTION_ACTIVE_SLAVE, \
-        NM_SETTING_BOND_OPTION_ARP_IP_TARGET
+        NM_SETTING_BOND_OPTION_ARP_IP_TARGET, NM_SETTING_BOND_OPTION_NS_IP6_TARGET
 
 /*****************************************************************************/
 
@@ -267,7 +269,7 @@ set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new
 
     cur_strv =
         nm_strsplit_set_full(cur_arp_ip_target, NM_ASCII_SPACES, NM_STRSPLIT_SET_FLAGS_STRSTRIP);
-    new_strv = nm_utils_bond_option_arp_ip_targets_split(new_arp_ip_target);
+    new_strv = nm_utils_bond_option_ip_split(new_arp_ip_target);
 
     cur_len = NM_PTRARRAY_LEN(cur_strv);
     new_len = NM_PTRARRAY_LEN(new_strv);
@@ -364,7 +366,7 @@ _bond_arp_ip_target_to_platform(const char *value, in_addr_t out[static NM_BOND_
     int                  i;
     int                  added = 0;
 
-    ip = nm_utils_bond_option_arp_ip_targets_split(value);
+    ip = nm_utils_bond_option_ip_split(value);
 
     if (!ip)
         return added;
@@ -380,6 +382,31 @@ _bond_arp_ip_target_to_platform(const char *value, in_addr_t out[static NM_BOND_
     return added;
 }
 
+static guint8
+_bond_ns_ip6_target_to_platform(const char     *value,
+                                struct in6_addr out[static NM_BOND_MAX_ARP_TARGETS])
+{
+    gs_free const char **ip = NULL;
+    struct in6_addr      in6_a;
+    int                  i;
+    int                  added = 0;
+
+    ip = nm_utils_bond_option_ip_split(value);
+
+    if (!ip)
+        return added;
+
+    for (i = 0; ip[i]; i++) {
+        if (added > NM_BOND_MAX_ARP_TARGETS - 1)
+            break;
+        if (!nm_inet_parse_bin(AF_INET6, ip[i], NULL, &in6_a))
+            nm_assert_not_reached(); /* verify() already validated the IP addresses */
+
+        out[added++] = in6_a;
+    }
+    return added;
+}
+
 static int
 _setting_bond_primary_opt_as_ifindex(NMSettingBond *s_bond)
 {
@@ -436,6 +463,10 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
                                    NM_SETTING_BOND_OPTION_XMIT_HASH_POLICY),
         .num_grat_arp      = _v_u8(s_bond, NM_SETTING_BOND_OPTION_NUM_GRAT_ARP),
         .all_ports_active  = _v_u8(s_bond, NM_SETTING_BOND_OPTION_ALL_SLAVES_ACTIVE),
+        .arp_missed_max    = _v_u8(s_bond, NM_SETTING_BOND_OPTION_ARP_MISSED_MAX),
+        .lacp_active       = _v_fcn(_nm_setting_bond_lacp_active_from_string,
+                              s_bond,
+                              NM_SETTING_BOND_OPTION_LACP_ACTIVE),
         .lacp_rate         = _v_fcn(_nm_setting_bond_lacp_rate_from_string,
                             s_bond,
                             NM_SETTING_BOND_OPTION_LACP_RATE),
@@ -455,6 +486,11 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
         props->arp_ip_targets_num =
             _bond_arp_ip_target_to_platform(opt_value, props->arp_ip_target);
 
+    opt_value = nm_setting_bond_get_option_normalized(s_bond, NM_SETTING_BOND_OPTION_NS_IP6_TARGET);
+    if (opt_value != NULL)
+        props->ns_ip6_targets_num =
+            _bond_ns_ip6_target_to_platform(opt_value, props->ns_ip6_target);
+
     props->miimon_has           = !props->arp_interval && !props->arp_validate;
     props->updelay_has          = props->miimon_has && props->miimon;
     props->downdelay_has        = props->miimon_has && props->miimon;
@@ -462,6 +498,7 @@ _platform_lnk_bond_init_from_setting(NMSettingBond *s_bond, NMPlatformLnkBond *p
     props->resend_igmp_has      = props->resend_igmp != 1;
     props->lp_interval_has      = props->lp_interval != 1;
     props->tlb_dynamic_lb_has   = NM_IN_SET(props->mode, NM_BOND_MODE_TLB, NM_BOND_MODE_ALB);
+    props->lacp_active_has      = NM_IN_SET(props->mode, NM_BOND_MODE_8023AD);
 }
 
 static void
@@ -639,12 +676,14 @@ commit_port_options(NMDevice *bond_device, NMDevice *port, NMSettingBondPort *s_
 
     nm_platform_link_change(nm_device_get_platform(port),
                             nm_device_get_ifindex(port),
+                            NULL,
                             &((NMPlatformLinkBondPort){
                                 .queue_id = s_port ? nm_setting_bond_port_get_queue_id(s_port)
                                                    : NM_BOND_PORT_QUEUE_ID_DEF,
                                 .prio     = prio_has ? prio : 0,
                                 .prio_has = prio_has,
-                            }));
+                            }),
+                            0);
 }
 
 static NMTernary
@@ -686,8 +725,13 @@ attach_port(NMDevice                  *device,
     return TRUE;
 }
 
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceBond *self = NM_DEVICE_BOND(device);
     gboolean      success;
@@ -749,6 +793,8 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
             _LOGI(LOGD_BOND, "bond port %s was detached", nm_device_get_ip_iface(port));
         }
     }
+
+    return TRUE;
 }
 
 static gboolean
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index c5ce34c2..9a45dbf3 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -101,13 +101,16 @@ check_connection_available(NMDevice                      *device,
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingBridge *s_bridge;
     const char      *mac_address;
 
     if (!NM_DEVICE_CLASS(nm_device_bridge_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     if (nm_connection_is_type(connection, NM_SETTING_BLUETOOTH_SETTING_NAME)
@@ -434,96 +437,6 @@ static const Option slave_options[] = {
     OPTION(NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE, "hairpin_mode", OPTION_TYPE_BOOL(FALSE), ),
     {0}};
 
-static void
-commit_option(NMDevice *device, NMSetting *setting, const Option *option, gboolean slave)
-{
-    int                         ifindex = nm_device_get_ifindex(device);
-    nm_auto_unset_gvalue GValue val     = G_VALUE_INIT;
-    GParamSpec                 *pspec;
-    const char                 *value;
-    char                        value_buf[100];
-
-    if (slave)
-        nm_assert(NM_IS_SETTING_BRIDGE_PORT(setting));
-    else
-        nm_assert(NM_IS_SETTING_BRIDGE(setting));
-
-    pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), option->name);
-    nm_assert(pspec);
-
-    g_value_init(&val, G_PARAM_SPEC_VALUE_TYPE(pspec));
-    g_object_get_property((GObject *) setting, option->name, &val);
-
-    if (option->to_sysfs) {
-        value = option->to_sysfs(&val);
-        goto out;
-    }
-
-    switch (pspec->value_type) {
-    case G_TYPE_BOOLEAN:
-        value = g_value_get_boolean(&val) ? "1" : "0";
-        break;
-    case G_TYPE_UINT64:
-    case G_TYPE_UINT:
-    {
-        guint64 uval;
-
-        if (pspec->value_type == G_TYPE_UINT64)
-            uval = g_value_get_uint64(&val);
-        else
-            uval = (guint) g_value_get_uint(&val);
-
-        /* zero means "unspecified" for some NM properties but isn't in the
-             * allowed kernel range, so reset the property to the default value.
-             */
-        if (option->default_if_zero && uval == 0) {
-            if (pspec->value_type == G_TYPE_UINT64)
-                uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT64(pspec);
-            else
-                uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT(pspec);
-        }
-
-        /* Linux kernel bridge interfaces use 'centiseconds' for time-based values.
-             * In reality it's not centiseconds, but depends on HZ and USER_HZ, which
-             * is almost always works out to be a multiplier of 100, so we can assume
-             * centiseconds.  See clock_t_to_jiffies().
-             */
-        if (option->user_hz_compensate)
-            uval *= 100;
-
-        if (pspec->value_type == G_TYPE_UINT64)
-            nm_sprintf_buf(value_buf, "%" G_GUINT64_FORMAT, uval);
-        else
-            nm_sprintf_buf(value_buf, "%u", (guint) uval);
-
-        value = value_buf;
-    } break;
-    case G_TYPE_STRING:
-        value = g_value_get_string(&val);
-        break;
-    default:
-        nm_assert_not_reached();
-        value = NULL;
-        break;
-    }
-
-out:
-    if (!value)
-        return;
-
-    if (slave) {
-        nm_platform_sysctl_slave_set_option(nm_device_get_platform(device),
-                                            ifindex,
-                                            option->sysname,
-                                            value);
-    } else {
-        nm_platform_sysctl_master_set_option(nm_device_get_platform(device),
-                                             ifindex,
-                                             option->sysname,
-                                             value);
-    }
-}
-
 static const NMPlatformBridgeVlan **
 setting_vlans_to_platform(GPtrArray *array)
 {
@@ -558,19 +471,92 @@ setting_vlans_to_platform(GPtrArray *array)
 }
 
 static void
-commit_slave_options(NMDevice *device, NMSettingBridgePort *setting)
+commit_port_options(NMDevice *device, NMSettingBridgePort *setting)
 {
     const Option              *option;
     NMSetting                 *s;
     gs_unref_object NMSetting *s_clear = NULL;
+    int                        ifindex = nm_device_get_ifindex(device);
 
     if (setting)
         s = NM_SETTING(setting);
     else
         s = s_clear = nm_setting_bridge_port_new();
 
-    for (option = slave_options; option->name; option++)
-        commit_option(device, s, option, TRUE);
+    for (option = slave_options; option->name; option++) {
+        nm_auto_unset_gvalue GValue val = G_VALUE_INIT;
+        GParamSpec                 *pspec;
+        const char                 *value;
+        char                        value_buf[100];
+
+        pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(s), option->name);
+        nm_assert(pspec);
+
+        g_value_init(&val, G_PARAM_SPEC_VALUE_TYPE(pspec));
+        g_object_get_property((GObject *) s, option->name, &val);
+
+        if (option->to_sysfs) {
+            value = option->to_sysfs(&val);
+            goto out;
+        }
+
+        switch (pspec->value_type) {
+        case G_TYPE_BOOLEAN:
+            value = g_value_get_boolean(&val) ? "1" : "0";
+            break;
+        case G_TYPE_UINT64:
+        case G_TYPE_UINT:
+        {
+            guint64 uval;
+
+            if (pspec->value_type == G_TYPE_UINT64)
+                uval = g_value_get_uint64(&val);
+            else
+                uval = (guint) g_value_get_uint(&val);
+
+            /* zero means "unspecified" for some NM properties but isn't in the
+             * allowed kernel range, so reset the property to the default value.
+             */
+            if (option->default_if_zero && uval == 0) {
+                if (pspec->value_type == G_TYPE_UINT64)
+                    uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT64(pspec);
+                else
+                    uval = NM_G_PARAM_SPEC_GET_DEFAULT_UINT(pspec);
+            }
+
+            /* Linux kernel bridge interfaces use 'centiseconds' for time-based values.
+             * In reality it's not centiseconds, but depends on HZ and USER_HZ, which
+             * is almost always works out to be a multiplier of 100, so we can assume
+             * centiseconds.  See clock_t_to_jiffies().
+             */
+            if (option->user_hz_compensate)
+                uval *= 100;
+
+            if (pspec->value_type == G_TYPE_UINT64)
+                nm_sprintf_buf(value_buf, "%" G_GUINT64_FORMAT, uval);
+            else
+                nm_sprintf_buf(value_buf, "%u", (guint) uval);
+
+            value = value_buf;
+        } break;
+        case G_TYPE_STRING:
+            value = g_value_get_string(&val);
+            break;
+        default:
+            nm_assert_not_reached();
+            value = NULL;
+            break;
+        }
+
+out:
+        if (!value)
+            return;
+
+        nm_platform_sysctl_slave_set_option(nm_device_get_platform(device),
+                                            ifindex,
+                                            option->sysname,
+                                            value);
+    }
 }
 
 static void
@@ -746,8 +732,13 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge)
     enabled = nm_setting_bridge_get_vlan_filtering(s_bridge);
 
     if (!enabled) {
-        nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0");
-        nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "1");
+        nm_platform_link_set_bridge_info(
+            plat,
+            ifindex,
+            &((NMPlatformLinkSetBridgeInfoData){.vlan_filtering_has    = TRUE,
+                                                .vlan_filtering_val    = FALSE,
+                                                .vlan_default_pvid_has = TRUE,
+                                                .vlan_default_pvid_val = 1}));
         nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL);
         return TRUE;
     }
@@ -762,14 +753,17 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge)
 
     self->vlan_configured = TRUE;
 
-    /* Filtering must be disabled to change the default PVID */
-    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "0"))
-        return FALSE;
-
-    /* Clear the default PVID so that we later can force the re-creation of
+    /* Filtering must be disabled to change the default PVID.
+     * Clear the default PVID so that we later can force the re-creation of
      * default PVID VLANs by writing the option again. */
-    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", "0"))
-        return FALSE;
+
+    nm_platform_link_set_bridge_info(
+        plat,
+        ifindex,
+        &((NMPlatformLinkSetBridgeInfoData){.vlan_filtering_has    = TRUE,
+                                            .vlan_filtering_val    = FALSE,
+                                            .vlan_default_pvid_has = TRUE,
+                                            .vlan_default_pvid_val = 0}));
 
     /* Clear all existing VLANs */
     if (!nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, NULL))
@@ -779,11 +773,11 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge)
      * a PVID VLAN on each port, including the bridge itself. */
     pvid = nm_setting_bridge_get_vlan_default_pvid(s_bridge);
     if (pvid) {
-        char value[32];
-
-        nm_sprintf_buf(value, "%u", pvid);
-        if (!nm_platform_sysctl_master_set_option(plat, ifindex, "default_pvid", value))
-            return FALSE;
+        nm_platform_link_set_bridge_info(
+            plat,
+            ifindex,
+            &((NMPlatformLinkSetBridgeInfoData){.vlan_default_pvid_has = TRUE,
+                                                .vlan_default_pvid_val = pvid}));
     }
 
     /* Create VLANs only after setting the default PVID, so that
@@ -793,8 +787,12 @@ bridge_set_vlan_options(NMDevice *device, NMSettingBridge *s_bridge)
     if (plat_vlans && !nm_platform_link_set_bridge_vlans(plat, ifindex, FALSE, plat_vlans))
         return FALSE;
 
-    if (!nm_platform_sysctl_master_set_option(plat, ifindex, "vlan_filtering", "1"))
-        return FALSE;
+    nm_platform_link_set_bridge_info(plat,
+                                     ifindex,
+                                     &((NMPlatformLinkSetBridgeInfoData){
+                                         .vlan_filtering_has = TRUE,
+                                         .vlan_filtering_val = TRUE,
+                                     }));
 
     return TRUE;
 }
@@ -1027,7 +1025,7 @@ attach_port(NMDevice                  *device,
                 return FALSE;
         }
 
-        commit_slave_options(port, s_port);
+        commit_port_options(port, s_port);
 
         _LOGI(LOGD_BRIDGE, "attached bridge port %s", nm_device_get_ip_iface(port));
     } else {
@@ -1037,8 +1035,13 @@ attach_port(NMDevice                  *device,
     return TRUE;
 }
 
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceBridge *self = NM_DEVICE_BRIDGE(device);
     gboolean        success;
@@ -1055,7 +1058,7 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
 
     if (ifindex_slave <= 0) {
         _LOGD(LOGD_TEAM, "bridge port %s is already detached", nm_device_get_ip_iface(port));
-        return;
+        return TRUE;
     }
 
     if (configure) {
@@ -1071,6 +1074,8 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
     } else {
         _LOGI(LOGD_BRIDGE, "bridge port %s was detached", nm_device_get_ip_iface(port));
     }
+
+    return TRUE;
 }
 
 static gboolean
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 97cf84a1..aedacc24 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -344,13 +344,16 @@ match_subchans(NMDeviceEthernet *self, NMSettingWired *s_wired, gboolean *try_ma
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceEthernet *self = NM_DEVICE_ETHERNET(device);
     NMSettingWired   *s_wired;
 
     if (!NM_DEVICE_CLASS(nm_device_ethernet_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     if (nm_connection_is_type(connection, NM_SETTING_PPPOE_SETTING_NAME)
diff --git a/src/core/devices/nm-device-factory.h b/src/core/devices/nm-device-factory.h
index ac5ae05f..fc3d9dd4 100644
--- a/src/core/devices/nm-device-factory.h
+++ b/src/core/devices/nm-device-factory.h
@@ -208,8 +208,7 @@ 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.c b/src/core/devices/nm-device-generic.c
index 9f85925b..c0dcf0de 100644
--- a/src/core/devices/nm-device-generic.c
+++ b/src/core/devices/nm-device-generic.c
@@ -16,7 +16,7 @@
 NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_TYPE_DESCRIPTION, );
 
 typedef struct {
-    char *type_description;
+    const char *type_description;
 } NMDeviceGenericPrivate;
 
 struct _NMDeviceGeneric {
@@ -64,20 +64,23 @@ realize_start_notify(NMDevice *device, const NMPlatformLink *plink)
 
     NM_DEVICE_CLASS(nm_device_generic_parent_class)->realize_start_notify(device, plink);
 
-    nm_clear_g_free(&priv->type_description);
     ifindex = nm_device_get_ip_ifindex(NM_DEVICE(self));
-    if (ifindex > 0)
+    if (ifindex > 0) {
         priv->type_description =
-            g_strdup(nm_platform_link_get_type_name(nm_device_get_platform(device), ifindex));
+            nm_platform_link_get_type_name(nm_device_get_platform(device), ifindex);
+    }
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingConnection *s_con;
 
     if (!NM_DEVICE_CLASS(nm_device_generic_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_con = nm_connection_get_setting_connection(connection);
@@ -125,22 +128,6 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     }
 }
 
-static void
-set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
-{
-    NMDeviceGeneric        *self = NM_DEVICE_GENERIC(object);
-    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
-
-    switch (prop_id) {
-    case PROP_TYPE_DESCRIPTION:
-        priv->type_description = g_value_dup_string(value);
-        break;
-    default:
-        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
-        break;
-    }
-}
-
 /*****************************************************************************/
 
 static void
@@ -177,17 +164,6 @@ nm_device_generic_new(const NMPlatformLink *plink, gboolean nm_plugin_missing)
                         NULL);
 }
 
-static void
-dispose(GObject *object)
-{
-    NMDeviceGeneric        *self = NM_DEVICE_GENERIC(object);
-    NMDeviceGenericPrivate *priv = NM_DEVICE_GENERIC_GET_PRIVATE(self);
-
-    nm_clear_g_free(&priv->type_description);
-
-    G_OBJECT_CLASS(nm_device_generic_parent_class)->dispose(object);
-}
-
 static const NMDBusInterfaceInfoExtended interface_info_device_generic = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_GENERIC,
@@ -207,9 +183,7 @@ nm_device_generic_class_init(NMDeviceGenericClass *klass)
     NMDeviceClass     *device_class      = NM_DEVICE_CLASS(klass);
 
     object_class->constructor  = constructor;
-    object_class->dispose      = dispose;
     object_class->get_property = get_property;
-    object_class->set_property = set_property;
 
     dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_generic);
 
@@ -228,7 +202,7 @@ nm_device_generic_class_init(NMDeviceGenericClass *klass)
                             "",
                             "",
                             NULL,
-                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 }
diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c
index d025931e..6ce61d0b 100644
--- a/src/core/devices/nm-device-infiniband.c
+++ b/src/core/devices/nm-device-infiniband.c
@@ -87,7 +87,7 @@ act_stage1_prepare(NMDevice *device, NMDeviceStateReason *out_failure_reason)
     /* With some drivers the interface must be down to set transport mode */
     nm_device_take_down(device, TRUE);
     ok = nm_platform_sysctl_set(nm_device_get_platform(device),
-                                NMP_SYSCTL_PATHID_NETDIR(dirfd, ifname_verified, "mode"),
+                                NMP_SYSCTL_PATHID_NETDIR_A(dirfd, ifname_verified, "mode"),
                                 transport_mode);
     nm_device_bring_up(device);
 
@@ -108,15 +108,18 @@ get_configured_mtu(NMDevice *device, NMDeviceMtuSource *out_source, gboolean *ou
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingInfiniband *s_infiniband;
 
     if (!NM_DEVICE_CLASS(nm_device_infiniband_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         const char *mac;
         const char *hw_addr;
 
diff --git a/src/core/devices/nm-device-ip-tunnel.c b/src/core/devices/nm-device-ip-tunnel.c
index a5760bf4..cc62180e 100644
--- a/src/core/devices/nm-device-ip-tunnel.c
+++ b/src/core/devices/nm-device-ip-tunnel.c
@@ -498,7 +498,10 @@ update_connection(NMDevice *device, NMConnection *connection)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceIPTunnel        *self = NM_DEVICE_IP_TUNNEL(device);
     NMDeviceIPTunnelPrivate *priv = NM_DEVICE_IP_TUNNEL_GET_PRIVATE(self);
@@ -507,7 +510,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     const char              *parent;
 
     if (!NM_DEVICE_CLASS(nm_device_ip_tunnel_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_ip_tunnel = nm_connection_get_setting_ip_tunnel(connection);
@@ -520,7 +523,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
         return FALSE;
     }
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         /* Check parent interface; could be an interface name or a UUID */
         parent = nm_setting_ip_tunnel_get_parent(s_ip_tunnel);
         if (parent && !nm_device_match_parent(device, parent)) {
diff --git a/src/core/devices/nm-device-logging.h b/src/core/devices/nm-device-logging.h
index ffc90c2d..53330b5e 100644
--- a/src/core/devices/nm-device-logging.h
+++ b/src/core/devices/nm-device-logging.h
@@ -11,37 +11,37 @@
 #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)))
+#define _NM_DEVICE_CAST(self) _Generic((self), NMDevice *: ((NMDevice *) (self)))
 #else
-#define _NM_DEVICE_CAST(self)                    \
-    _Generic((self), _NMLOG_DEVICE_TYPE *        \
-             : ((NMDevice *) (self)), NMDevice * \
-             : ((NMDevice *) (self)))
+#define _NM_DEVICE_CAST(self) \
+    _Generic((self), _NMLOG_DEVICE_TYPE *: ((NMDevice *) (self)), NMDevice *: ((NMDevice *) (self)))
 #endif
 
 #undef _NMLOG_ENABLED
 #define _NMLOG_ENABLED(level, domain) (nm_logging_enabled((level), (domain)))
-#define _NMLOG(level, domain, ...)                                                       \
-    G_STMT_START                                                                         \
-    {                                                                                    \
-        const NMLogLevel  _level  = (level);                                             \
-        const NMLogDomain _domain = (domain);                                            \
-                                                                                         \
-        if (nm_logging_enabled(_level, _domain)) {                                       \
-            typeof(*self) *const _self   = (self);                                       \
-            const char *const    _ifname = _nm_device_get_iface(_NM_DEVICE_CAST(_self)); \
-                                                                                         \
-            nm_log_obj(_level,                                                           \
-                       _domain,                                                          \
-                       _ifname,                                                          \
-                       NULL,                                                             \
-                       _self,                                                            \
-                       "device",                                                         \
-                       "%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                    \
-                       NM_PRINT_FMT_QUOTED(_ifname, "(", _ifname, ")", "[null]")         \
-                           _NM_UTILS_MACRO_REST(__VA_ARGS__));                           \
-        }                                                                                \
-    }                                                                                    \
+#define _NMLOG(level, domain, ...)                                                                  \
+    G_STMT_START                                                                                    \
+    {                                                                                               \
+        const NMLogLevel  _level  = (level);                                                        \
+        const NMLogDomain _domain = (domain);                                                       \
+                                                                                                    \
+        if (nm_logging_enabled(_level, _domain)) {                                                  \
+            typeof(*self) *const _self   = (self);                                                  \
+            const char *const    _ifname = _nm_device_get_iface(_NM_DEVICE_CAST(_self));            \
+            const char          *_type   = nm_device_get_type_desc_for_log(_NM_DEVICE_CAST(_self)); \
+                                                                                                    \
+            nm_log_obj(_level,                                                                      \
+                       _domain,                                                                     \
+                       _ifname,                                                                     \
+                       NULL,                                                                        \
+                       _self,                                                                       \
+                       "device",                                                                    \
+                       "%s%s%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                         \
+                       NM_PRINT_FMT_QUOTED(_ifname, "(", _ifname, ")", "[null]"),                   \
+                       NM_PRINT_FMT_QUOTED(_type, "[", _type, "]", "")                              \
+                           _NM_UTILS_MACRO_REST(__VA_ARGS__));                                      \
+        }                                                                                           \
+    }                                                                                               \
     G_STMT_END
 
 #endif /* __NETWORKMANAGER_DEVICE_LOGGING_H__ */
diff --git a/src/core/devices/nm-device-macvlan.c b/src/core/devices/nm-device-macvlan.c
index 3f57bfb1..8cdef0cf 100644
--- a/src/core/devices/nm-device-macvlan.c
+++ b/src/core/devices/nm-device-macvlan.c
@@ -208,14 +208,22 @@ create_and_realize(NMDevice              *device,
     s_macvlan = nm_connection_get_setting_macvlan(connection);
     g_return_val_if_fail(s_macvlan, FALSE);
 
-    parent_ifindex = parent ? nm_device_get_ifindex(parent) : 0;
+    if (!parent) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
+                    "MACVLAN device can not be created without a parent interface");
+        return FALSE;
+    }
 
+    parent_ifindex = nm_device_get_ifindex(parent);
     if (parent_ifindex <= 0) {
         g_set_error(error,
                     NM_DEVICE_ERROR,
                     NM_DEVICE_ERROR_MISSING_DEPENDENCIES,
-                    "MACVLAN devices can not be created without a parent interface");
-        g_return_val_if_fail(!parent, FALSE);
+                    "cannot retrieve ifindex of interface %s (%s)",
+                    nm_device_get_iface(parent),
+                    nm_device_get_type_desc(parent));
         return FALSE;
     }
 
@@ -274,14 +282,17 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
 /*****************************************************************************/
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceMacvlanPrivate *priv = NM_DEVICE_MACVLAN_GET_PRIVATE(device);
     NMSettingMacvlan       *s_macvlan;
     const char             *parent = NULL;
 
     if (!NM_DEVICE_CLASS(nm_device_macvlan_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_macvlan = nm_connection_get_setting_macvlan(connection);
@@ -300,7 +311,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     }
 
     /* Before the device is realized some properties will not be set */
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         if (setting_mode_to_platform(nm_setting_macvlan_get_mode(s_macvlan)) != priv->props.mode) {
             nm_utils_error_set_literal(error,
                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c
index 1860df3c..27566607 100644
--- a/src/core/devices/nm-device-ppp.c
+++ b/src/core/devices/nm-device-ppp.c
@@ -149,12 +149,15 @@ _ppp_mgr_callback(NMPppMgr *ppp_mgr, const NMPppMgrCallbackData *callback_data,
 /*****************************************************************************/
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingPppoe *s_pppoe;
 
     if (!NM_DEVICE_CLASS(nm_device_ppp_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_pppoe = nm_connection_get_setting_pppoe(connection);
diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
index c597e052..013bc7fd 100644
--- a/src/core/devices/nm-device-private.h
+++ b/src/core/devices/nm-device-private.h
@@ -29,10 +29,6 @@ enum NMActStageReturn {
 
 #define NM_DEVICE_CAP_INTERNAL_MASK 0xc0000000
 
-NMSettings *nm_device_get_settings(NMDevice *self);
-
-NMManager *nm_device_get_manager(NMDevice *self);
-
 gboolean nm_device_set_ip_ifindex(NMDevice *self, int ifindex);
 
 gboolean nm_device_set_ip_iface(NMDevice *self, const char *iface);
@@ -180,4 +176,6 @@ void nm_device_auth_request(NMDevice                      *self,
                             NMManagerDeviceAuthRequestFunc callback,
                             gpointer                       user_data);
 
+void nm_device_link_properties_set(NMDevice *self, gboolean reapply);
+
 #endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c
index cbea7d7d..430e62aa 100644
--- a/src/core/devices/nm-device-tun.c
+++ b/src/core/devices/nm-device-tun.c
@@ -284,7 +284,10 @@ _same_og(const char *str, gboolean og_valid, guint32 og_num)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceTun        *self = NM_DEVICE_TUN(device);
     NMDeviceTunPrivate *priv = NM_DEVICE_TUN_GET_PRIVATE(self);
@@ -292,10 +295,10 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     NMSettingTun       *s_tun;
 
     if (!NM_DEVICE_CLASS(nm_device_tun_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         switch (priv->props.type) {
         case IFF_TUN:
             mode = NM_SETTING_TUN_MODE_TUN;
diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c
index feb011db..7849e724 100644
--- a/src/core/devices/nm-device-vlan.c
+++ b/src/core/devices/nm-device-vlan.c
@@ -303,17 +303,20 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
 /*****************************************************************************/
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceVlanPrivate *priv = NM_DEVICE_VLAN_GET_PRIVATE(device);
     NMSettingVlan       *s_vlan;
     const char          *parent;
 
     if (!NM_DEVICE_CLASS(nm_device_vlan_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         s_vlan = nm_connection_get_setting_vlan(connection);
 
         if (nm_setting_vlan_get_id(s_vlan) != priv->vlan_id) {
diff --git a/src/core/devices/nm-device-vrf.c b/src/core/devices/nm-device-vrf.c
index ad31f3a2..a13de1cb 100644
--- a/src/core/devices/nm-device-vrf.c
+++ b/src/core/devices/nm-device-vrf.c
@@ -142,16 +142,19 @@ create_and_realize(NMDevice              *device,
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceVrfPrivate *priv = NM_DEVICE_VRF_GET_PRIVATE(device);
     NMSettingVrf       *s_vrf;
 
     if (!NM_DEVICE_CLASS(nm_device_vrf_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         s_vrf = _nm_connection_get_setting(connection, NM_TYPE_SETTING_VRF);
 
         if (priv->props.table != nm_setting_vrf_get_table(s_vrf)) {
@@ -238,8 +241,13 @@ attach_port(NMDevice                  *device,
     return TRUE;
 }
 
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceVrf *self = NM_DEVICE_VRF(device);
     gboolean     success;
@@ -274,6 +282,8 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
             _LOGI(LOGD_DEVICE, "VRF port %s was detached", nm_device_get_ip_iface(port));
         }
     }
+
+    return TRUE;
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
index 44a7be33..061ee3f2 100644
--- a/src/core/devices/nm-device-vxlan.c
+++ b/src/core/devices/nm-device-vxlan.c
@@ -238,17 +238,20 @@ address_matches(const char *candidate, in_addr_t addr4, struct in6_addr *addr6)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceVxlanPrivate *priv = NM_DEVICE_VXLAN_GET_PRIVATE(device);
     NMSettingVxlan       *s_vxlan;
     const char           *parent;
 
     if (!NM_DEVICE_CLASS(nm_device_vxlan_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
-    if (nm_device_is_real(device)) {
+    if (check_properties && nm_device_is_real(device)) {
         s_vxlan = nm_connection_get_setting_vxlan(connection);
 
         parent = nm_setting_vxlan_get_parent(s_vxlan);
diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c
index 98356ccf..282eea87 100644
--- a/src/core/devices/nm-device-wpan.c
+++ b/src/core/devices/nm-device-wpan.c
@@ -75,13 +75,16 @@ update_connection(NMDevice *device, NMConnection *connection)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingWpan *s_wpan;
     const char    *mac, *hw_addr;
 
     if (!NM_DEVICE_CLASS(nm_device_wpan_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_wpan = NM_SETTING_WPAN(nm_connection_get_setting(connection, NM_TYPE_SETTING_WPAN));
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 62a9ff1e..2038e2f2 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -91,8 +91,8 @@
 
 #define GRACE_PERIOD_MULTIPLIER 2U
 
-#define CARRIER_WAIT_TIME_MS           6000
-#define CARRIER_WAIT_TIME_AFTER_MTU_MS 10000
+#define CARRIER_WAIT_TIME_MS             6000
+#define CARRIER_WAIT_TIME_AFTER_MTU_MSEC 10000
 
 #define NM_DEVICE_AUTH_RETRIES_UNSET    -1
 #define NM_DEVICE_AUTH_RETRIES_INFINITY -2
@@ -132,11 +132,6 @@ typedef struct {
 } SlaveInfo;
 
 typedef struct {
-    NMDevice *device;
-    guint     idle_add_id;
-} DeleteOnDeactivateData;
-
-typedef struct {
     NMDevice               *device;
     GCancellable           *cancellable;
     NMPlatformAsyncCallback callback;
@@ -335,7 +330,6 @@ enum {
     IP6_PREFIX_DELEGATED,
     IP6_SUBNET_NEEDED,
     REMOVED,
-    RECHECK_AUTO_ACTIVATE,
     RECHECK_ASSUME,
     DNS_LOOKUP_DONE,
     PLATFORM_ADDRESS_CHANGED,
@@ -513,8 +507,8 @@ typedef struct _NMDevicePrivate {
 
     NMUnmanagedFlags unmanaged_mask;
     NMUnmanagedFlags unmanaged_flags;
-    DeleteOnDeactivateData
-        *delete_on_deactivate_data; /* data for scheduled cleanup when deleting link (g_idle_add) */
+
+    GSource *delete_on_deactivate_idle_source;
 
     GCancellable *deactivating_cancellable;
 
@@ -542,10 +536,10 @@ typedef struct _NMDevicePrivate {
     /* Link stuff */
     guint             link_connected_id;
     guint             link_disconnected_id;
-    guint             carrier_defer_id;
-    guint             carrier_wait_id;
     gulong            config_changed_id;
     gulong            ifindex_changed_id;
+    GSource          *carrier_wait_source;
+    GSource          *carrier_defer_source;
     guint32           mtu;
     guint32           ip6_mtu; /* FIXME(l3cfg) */
     guint32           mtu_initial;
@@ -559,9 +553,9 @@ typedef struct _NMDevicePrivate {
      * until taking action.
      *
      * When changing MTU, the device might take longer then that. So, whenever
-     * NM changes the MTU it sets @carrier_wait_until_ms to CARRIER_WAIT_TIME_AFTER_MTU_MS
+     * NM changes the MTU it sets @carrier_wait_until_msec to CARRIER_WAIT_TIME_AFTER_MTU_MSEC
      * in the future. This is used to extend the grace period in this particular case. */
-    gint64 carrier_wait_until_ms;
+    gint64 carrier_wait_until_msec;
 
     union {
         struct {
@@ -576,6 +570,8 @@ typedef struct _NMDevicePrivate {
         NMDeviceSysIfaceState       sys_iface_state_;
     };
 
+    NMDeviceSysIfaceState sys_iface_state_before_sleep;
+
     bool carrier : 1;
     bool ignore_carrier : 1;
 
@@ -599,6 +595,8 @@ typedef struct _NMDevicePrivate {
 
     bool tc_committed : 1;
 
+    bool link_props_set : 1;
+
     NMDeviceStageState stage1_sriov_state : 3;
 
     char *current_stable_id;
@@ -702,6 +700,10 @@ typedef struct _NMDevicePrivate {
     GHashTable *ip6_saved_properties;
 
     EthtoolState *ethtool_state;
+    struct {
+        NMPlatformLinkProps       props;
+        NMPlatformLinkChangeFlags flags;
+    } link_props_state;
 
     /* master interface for bridge/bond/team slave */
     NMDevice *master;
@@ -743,6 +745,9 @@ typedef struct _NMDevicePrivate {
     guint   check_delete_unrealized_id;
     guint32 interface_flags;
 
+    guint32             port_detach_count;
+    NMDeviceStateReason port_detach_reason;
+
     struct {
         SriovOp *pending; /* SR-IOV operation currently running */
         SriovOp *next;    /* next SR-IOV operation scheduled */
@@ -863,6 +868,7 @@ static void sriov_op_cb(GError *error, gpointer user_data);
 static void device_ifindex_changed_cb(NMManager *manager, NMDevice *device_changed, NMDevice *self);
 static gboolean device_link_changed(gpointer user_data);
 static gboolean _get_maybe_ipv6_disabled(NMDevice *self);
+static void     deactivate_ready(NMDevice *self, NMDeviceStateReason reason);
 
 /*****************************************************************************/
 
@@ -2750,6 +2756,152 @@ _ethtool_state_set(NMDevice *self)
         priv->ethtool_state = g_steal_pointer(&ethtool_state);
 }
 
+static NMPlatformLinkChangeFlags
+link_properties_fill_from_setting(NMDevice *self, NMPlatformLinkProps *props)
+{
+    NMPlatformLinkChangeFlags flags = NM_PLATFORM_LINK_CHANGE_NONE;
+    NMSettingLink            *s_link;
+    gint64                    v;
+
+    *props = (NMPlatformLinkProps){};
+
+    s_link = nm_device_get_applied_setting(self, NM_TYPE_SETTING_LINK);
+    if (!s_link)
+        return 0;
+
+    v = nm_setting_link_get_tx_queue_length(s_link);
+    if (v != -1) {
+        props->tx_queue_length = (guint32) v;
+        flags |= NM_PLATFORM_LINK_CHANGE_TX_QUEUE_LENGTH;
+    }
+
+    v = nm_setting_link_get_gso_max_size(s_link);
+    if (v != -1) {
+        props->gso_max_size = (guint32) v;
+        flags |= NM_PLATFORM_LINK_CHANGE_GSO_MAX_SIZE;
+    }
+
+    v = nm_setting_link_get_gso_max_segments(s_link);
+    if (v != -1) {
+        props->gso_max_segments = (guint32) v;
+        flags |= NM_PLATFORM_LINK_CHANGE_GSO_MAX_SEGMENTS;
+    }
+
+    v = nm_setting_link_get_gro_max_size(s_link);
+    if (v != -1) {
+        props->gro_max_size = (guint32) v;
+        flags |= NM_PLATFORM_LINK_CHANGE_GRO_MAX_SIZE;
+    }
+
+    return flags;
+}
+
+void
+nm_device_link_properties_set(NMDevice *self, gboolean reapply)
+{
+    NMDevicePrivate          *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatformLinkProps       props;
+    NMPlatformLinkChangeFlags flags;
+    NMPlatform               *platform;
+    const NMPlatformLink     *plink;
+    int                       ifindex;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        return;
+
+    if (priv->link_props_set && !reapply)
+        return;
+
+    priv->link_props_set = TRUE;
+
+    flags = link_properties_fill_from_setting(self, &props);
+
+    if (flags == NM_PLATFORM_LINK_CHANGE_NONE
+        && priv->link_props_state.flags == NM_PLATFORM_LINK_CHANGE_NONE) {
+        /* Nothing to set now, and nothing was set previously. */
+        return;
+    }
+
+    platform = nm_device_get_platform(self);
+
+    if (priv->link_props_state.flags == NM_PLATFORM_LINK_CHANGE_NONE) {
+        /* It's the first time we reach here. Try to fetch the current
+         * link settings (reset them later). */
+        plink = nm_platform_link_get(platform, ifindex);
+        if (plink) {
+            priv->link_props_state.props = plink->link_props;
+            priv->link_props_state.flags = flags;
+        } else {
+            /* Unknown properties. The "priv->link_props_state.flags" stays unset.
+             * It indicates that "priv->link_props_state.props" is unknown. */
+        }
+
+    } else {
+        /* From a previous call we have some "priv->link_props_state.flags"
+         * flags, which indicates that all link props are cached. Also add
+         * "flags" which are are going to set, to indicate that those flags
+         * will need to be reset later. */
+        priv->link_props_state.flags |= flags;
+    }
+
+#define _RESET(_f, _field)                                                                \
+    if (!NM_FLAGS_HAS(flags, (_f)) && NM_FLAGS_HAS(priv->link_props_state.flags, (_f))) { \
+        props._field = priv->link_props_state.props._field;                               \
+        priv->link_props_state.flags &= ~(_f);                                            \
+        flags |= (_f);                                                                    \
+    }
+
+    /* During reapply, if we previously set some "priv->link_props_state.flags"
+     * but now not anymore (according to "flags"), then we reset the value now.
+     *
+     * We do this by copying the props field from "priv->link_props_state" to
+     * "props", reset the flag in "priv->link_props_state.flags" and set the
+     * flag in "flags" (for changing it). */
+    _RESET(NM_PLATFORM_LINK_CHANGE_TX_QUEUE_LENGTH, tx_queue_length);
+    _RESET(NM_PLATFORM_LINK_CHANGE_GSO_MAX_SIZE, gso_max_size);
+    _RESET(NM_PLATFORM_LINK_CHANGE_GSO_MAX_SEGMENTS, gso_max_segments);
+    _RESET(NM_PLATFORM_LINK_CHANGE_GRO_MAX_SIZE, gro_max_size);
+
+    if (nm_platform_link_change(platform, ifindex, &props, NULL, flags)) {
+        _LOGD(LOGD_DEVICE, "link properties successfully set");
+    } else {
+        _LOGW(LOGD_DEVICE, "failure setting link properties");
+    }
+}
+
+static void
+link_properties_reset(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+    NMPlatform      *platform;
+    int              ifindex;
+
+    if (priv->link_props_state.flags == 0)
+        goto out;
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0)
+        goto out;
+
+    platform = nm_device_get_platform(self);
+    nm_assert(platform);
+
+    if (nm_platform_link_change(platform,
+                                ifindex,
+                                &priv->link_props_state.props,
+                                NULL,
+                                priv->link_props_state.flags)) {
+        _LOGD(LOGD_DEVICE, "link properties successfully reset");
+    } else {
+        _LOGW(LOGD_DEVICE, "failure resetting link properties");
+    }
+
+out:
+    priv->link_props_set         = FALSE;
+    priv->link_props_state.flags = 0;
+}
+
 /*****************************************************************************/
 
 gboolean
@@ -2892,6 +3044,7 @@ nm_device_sys_iface_state_set(NMDevice *self, NMDeviceSysIfaceState sys_iface_st
               nm_device_sys_iface_state_to_string(sys_iface_state));
         priv->sys_iface_state_ = sys_iface_state;
         _dev_l3_cfg_commit_type_reset(self);
+        nm_device_l3cfg_commit(self, NM_L3_CFG_COMMIT_TYPE_AUTO, FALSE);
     }
 
     /* this function only sets a flag, no immediate actions are initiated.
@@ -2901,6 +3054,22 @@ nm_device_sys_iface_state_set(NMDevice *self, NMDeviceSysIfaceState sys_iface_st
     nm_assert(priv->sys_iface_state == sys_iface_state);
 }
 
+void
+nm_device_notify_sleeping(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    priv->sys_iface_state_before_sleep = priv->sys_iface_state;
+}
+
+NMDeviceSysIfaceState
+nm_device_get_sys_iface_state_before_sleep(NMDevice *self)
+{
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
+
+    return priv->sys_iface_state_before_sleep;
+}
+
 static void
 _active_connection_set_state_flags_full(NMDevice              *self,
                                         NMActivationStateFlags flags,
@@ -3371,7 +3540,7 @@ _dev_ip_state_check(NMDevice *self, int addr_family)
                                 &s_is_pending,
                                 &s_is_failed);
 
-    has_tna = priv->l3cfg && nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, addr_family);
+    has_tna = priv->l3cfg && nm_l3cfg_has_failedobj_pending(priv->l3cfg, addr_family);
     if (has_tna)
         s_is_pending = TRUE;
 
@@ -3816,9 +3985,7 @@ after_merge_flags:
 }
 
 static gboolean
-_dev_l3_register_l3cds_add_config(NMDevice          *self,
-                                  L3ConfigDataType   l3cd_type,
-                                  NML3CfgConfigFlags flags)
+_dev_l3_register_l3cds_add_config(NMDevice *self, L3ConfigDataType l3cd_type)
 {
     NMDevicePrivate     *priv = NM_DEVICE_GET_PRIVATE(self);
     NML3ConfigMergeFlags merge_flags;
@@ -3841,7 +4008,7 @@ _dev_l3_register_l3cds_add_config(NMDevice          *self,
                                _prop_get_ipvx_dns_priority(self, AF_INET6),
                                acd_defend_type,
                                acd_timeout_msec,
-                               flags,
+                               NM_L3CFG_CONFIG_FLAGS_NONE,
                                merge_flags);
 }
 
@@ -3849,7 +4016,6 @@ static gboolean
 _dev_l3_register_l3cds_set_one_full(NMDevice             *self,
                                     L3ConfigDataType      l3cd_type,
                                     const NML3ConfigData *l3cd,
-                                    NML3CfgConfigFlags    flags,
                                     NMTernary             commit_sync)
 {
     NMDevicePrivate                         *priv     = NM_DEVICE_GET_PRIVATE(self);
@@ -3873,7 +4039,7 @@ _dev_l3_register_l3cds_set_one_full(NMDevice             *self,
 
     if (priv->l3cfg) {
         if (priv->l3cds[l3cd_type].d) {
-            if (_dev_l3_register_l3cds_add_config(self, l3cd_type, flags))
+            if (_dev_l3_register_l3cds_add_config(self, l3cd_type))
                 changed = TRUE;
         }
 
@@ -3897,11 +4063,7 @@ _dev_l3_register_l3cds_set_one(NMDevice             *self,
                                const NML3ConfigData *l3cd,
                                NMTernary             commit_sync)
 {
-    return _dev_l3_register_l3cds_set_one_full(self,
-                                               l3cd_type,
-                                               l3cd,
-                                               NM_L3CFG_CONFIG_FLAGS_NONE,
-                                               commit_sync);
+    return _dev_l3_register_l3cds_set_one_full(self, l3cd_type, l3cd, commit_sync);
 }
 
 static void
@@ -3956,7 +4118,7 @@ _dev_l3_register_l3cds(NMDevice *self,
         }
         if (is_external)
             continue;
-        if (_dev_l3_register_l3cds_add_config(self, i, NM_L3CFG_CONFIG_FLAGS_NONE))
+        if (_dev_l3_register_l3cds_add_config(self, i))
             changed = TRUE;
     }
 
@@ -4110,6 +4272,7 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
             _dev_ipshared4_spawn_dnsmasq(self);
             nm_clear_l3cd(&priv->ipshared_data_4.v4.l3cd);
         }
+        _dev_ip_state_check_async(self, AF_UNSPEC);
         _dev_ipmanual_check_ready(self);
         return;
     case NM_L3_CONFIG_NOTIFY_TYPE_IPV4LL_EVENT:
@@ -4119,10 +4282,6 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         return;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE:
         return;
-    case NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED:
-        /* we commit again. This way we try to configure the routes.*/
-        _dev_l3_cfg_commit(self, FALSE);
-        return;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
         if (NM_FLAGS_ANY(notify_data->platform_change_on_idle.obj_type_flags,
                          nmp_object_type_to_flags(NMP_OBJECT_TYPE_LINK)
@@ -4154,9 +4313,6 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
                  * 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);
@@ -4736,6 +4892,7 @@ nm_device_parent_find_for_connection(NMDevice *self, const char *current_setting
             && nm_device_check_connection_compatible(
                 parent_device,
                 nm_settings_connection_get_connection(parent_connection),
+                TRUE,
                 NULL))
             return current_setting_parent;
     }
@@ -4959,6 +5116,18 @@ nm_device_get_ip_iface_identifier(NMDevice           *self,
 }
 
 const char *
+nm_device_get_s390_subchannels(NMDevice *self)
+{
+    NMDeviceClass *klass;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NULL);
+
+    klass = NM_DEVICE_GET_CLASS(self);
+
+    return klass->get_s390_subchannels ? klass->get_s390_subchannels(self) : NULL;
+}
+
+const char *
 nm_device_get_driver(NMDevice *self)
 {
     g_return_val_if_fail(self != NULL, NULL);
@@ -5243,13 +5412,31 @@ nm_device_get_type_desc(NMDevice *self)
 }
 
 const char *
+nm_device_get_type_desc_for_log(NMDevice *self)
+{
+    const char *type;
+
+    type = nm_device_get_type_desc(self);
+
+    /* Some OVS device types (ports and bridges) are not backed by a kernel link, and
+     * they can have the same name of another device of a different type. In fact, it's
+     * quite common to assign the same name to the OVS bridge, the OVS port and the OVS
+     * interface. For this reason, also log the type in case of OVS devices to make the
+     * log message unambiguous. */
+    if (NM_STR_HAS_PREFIX(type, "Open vSwitch"))
+        return type;
+
+    return NULL;
+}
+
+const char *
 nm_device_get_type_description(NMDevice *self)
 {
     g_return_val_if_fail(self != NULL, NULL);
 
     /* Beware: this function should return the same
-     * value as nm_device_get_type_description() in libnm. */
-
+     * value as nm_device_get_type_description() in libnm.
+     * The returned string is static or interned */
     return NM_DEVICE_GET_CLASS(self)->get_type_description(self);
 }
 
@@ -6162,7 +6349,7 @@ attach_port_cb(NMDevice *self, GError *error, gpointer user_data)
  * nm_device_master_enslave_slave:
  * @self: the master device
  * @slave: the slave device to enslave
- * @connection: (allow-none): the slave device's connection
+ * @connection: (nullable): the slave device's connection
  *
  * If @self is capable of enslaving other devices (ie it's a bridge, bond, team,
  * etc) then this function enslaves @slave.
@@ -6206,6 +6393,21 @@ nm_device_master_enslave_slave(NMDevice *self, NMDevice *slave, NMConnection *co
     attach_port_done(self, slave, success);
 }
 
+static void
+detach_port_cb(NMDevice *self, GError *error, gpointer user_data)
+{
+    nm_auto_unref_object NMDevice *slave      = user_data;
+    NMDevicePrivate               *slave_priv = NM_DEVICE_GET_PRIVATE(slave);
+
+    nm_assert(slave_priv->port_detach_count > 0);
+
+    if (--slave_priv->port_detach_count == 0) {
+        if (slave_priv->state == NM_DEVICE_STATE_DEACTIVATING) {
+            deactivate_ready(slave, slave_priv->port_detach_reason);
+        }
+    }
+}
+
 /**
  * nm_device_master_release_slave:
  * @self: the master device
@@ -6262,10 +6464,20 @@ nm_device_master_release_slave(NMDevice           *self,
 
     /* first, let subclasses handle the release ... */
     if (info->slave_is_enslaved || nm_device_sys_iface_state_is_external(slave)
-        || release_type >= RELEASE_SLAVE_TYPE_CONFIG_FORCE)
-        NM_DEVICE_GET_CLASS(self)->detach_port(self,
-                                               slave,
-                                               release_type >= RELEASE_SLAVE_TYPE_CONFIG);
+        || release_type >= RELEASE_SLAVE_TYPE_CONFIG_FORCE) {
+        NMTernary ret;
+
+        ret = NM_DEVICE_GET_CLASS(self)->detach_port(self,
+                                                     slave,
+                                                     release_type >= RELEASE_SLAVE_TYPE_CONFIG,
+                                                     NULL,
+                                                     detach_port_cb,
+                                                     g_object_ref(slave));
+        if (ret == NM_TERNARY_DEFAULT) {
+            slave_priv->port_detach_count++;
+            slave_priv->port_detach_reason = reason;
+        }
+    }
 
     /* raise notifications about the release, including clearing is_enslaved. */
     nm_device_slave_notify_release(slave, reason, release_type);
@@ -6344,13 +6556,6 @@ _dev_unmanaged_check_external_down(NMDevice *self, gboolean only_if_unmanaged, g
     }
 
     ext_flags = _dev_unmanaged_is_external_down(self, FALSE);
-    if (ext_flags != NM_UNMAN_FLAG_OP_SET_UNMANAGED) {
-        /* Ensure the assume check is queued before any queued state changes
-         * from the transition to UNAVAILABLE.
-         */
-        nm_device_queue_recheck_assume(self);
-    }
-
     if (now) {
         nm_device_set_unmanaged_by_flags(self,
                                          NM_UNMANAGED_EXTERNAL_DOWN,
@@ -6434,6 +6639,8 @@ carrier_changed(NMDevice *self, gboolean carrier)
     }
 
     if (carrier) {
+        gboolean recheck_auto_activate = FALSE;
+
         if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) {
             nm_device_queue_state(self,
                                   NM_DEVICE_STATE_DISCONNECTED,
@@ -6444,8 +6651,18 @@ carrier_changed(NMDevice *self, gboolean carrier)
              * when the carrier appears, auto connections are rechecked for
              * the device.
              */
-            nm_device_emit_recheck_auto_activate(self);
+            recheck_auto_activate = TRUE;
         }
+        if (nm_manager_devcon_autoconnect_blocked_reason_set(
+                nm_device_get_manager(self),
+                self,
+                NULL,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
+                FALSE))
+            recheck_auto_activate = TRUE;
+
+        if (recheck_auto_activate)
+            nm_device_recheck_auto_activate_schedule(self);
     } else {
         if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) {
             if (priv->queued_state.id && priv->queued_state.state >= NM_DEVICE_STATE_DISCONNECTED)
@@ -6464,24 +6681,20 @@ carrier_disconnected_action_cb(gpointer user_data)
     NMDevice        *self = NM_DEVICE(user_data);
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    _LOGD(LOGD_DEVICE,
-          "carrier: link disconnected (calling deferred action) (id=%u)",
-          priv->carrier_defer_id);
+    _LOGD(LOGD_DEVICE, "carrier: link disconnected (calling deferred action)");
 
-    priv->carrier_defer_id = 0;
+    nm_clear_g_source_inst(&priv->carrier_defer_source);
     carrier_changed(self, FALSE);
-    return FALSE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
 carrier_disconnected_action_cancel(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
-    guint            id   = priv->carrier_defer_id;
 
-    if (nm_clear_g_source(&priv->carrier_defer_id)) {
-        _LOGD(LOGD_DEVICE, "carrier: link disconnected (canceling deferred action) (id=%u)", id);
-    }
+    if (nm_clear_g_source_inst(&priv->carrier_defer_source))
+        _LOGD(LOGD_DEVICE, "carrier: link disconnected (canceling deferred action)");
 }
 
 void
@@ -6509,28 +6722,29 @@ nm_device_set_carrier(NMDevice *self, gboolean carrier)
         NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier);
         carrier_changed(self, TRUE);
 
-        if (priv->carrier_wait_id) {
+        if (priv->carrier_wait_source) {
             nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
             _carrier_wait_check_queued_act_request(self);
         }
     } else {
-        if (priv->carrier_wait_id)
+        if (priv->carrier_wait_source)
             nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
         NM_DEVICE_GET_CLASS(self)->carrier_changed_notify(self, carrier);
         if (state <= NM_DEVICE_STATE_DISCONNECTED && !priv->queued_act_request) {
             _LOGD(LOGD_DEVICE, "carrier: link disconnected");
+            carrier_disconnected_action_cancel(self);
             carrier_changed(self, FALSE);
-        } else {
-            gint64 now_ms, until_ms;
+        } else if (!priv->carrier_defer_source) {
+            gint64 until_ms;
+            gint64 now_ms;
 
             now_ms   = nm_utils_get_monotonic_timestamp_msec();
-            until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
-            priv->carrier_defer_id =
-                g_timeout_add(until_ms - now_ms, carrier_disconnected_action_cb, self);
+            until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_msec);
+            priv->carrier_defer_source =
+                nm_g_timeout_add_source(until_ms - now_ms, carrier_disconnected_action_cb, self);
             _LOGD(LOGD_DEVICE,
-                  "carrier: link disconnected (deferring action for %ld milliseconds) (id=%u)",
-                  (long) (until_ms - now_ms),
-                  priv->carrier_defer_id);
+                  "carrier: link disconnected (deferring action for %ld milliseconds)",
+                  (long) (until_ms - now_ms));
         }
     }
 }
@@ -6682,6 +6896,37 @@ device_update_interface_flags(NMDevice *self, const NMPlatformLink *plink)
                              TRUE);
 }
 
+/*
+ * Returns the reason for managing a device. The suffix "external" indicates
+ * that the reason mainly depends on whether we want to make the device
+ * sys-iface-state=external or not.
+ */
+NMDeviceStateReason
+nm_device_get_manage_reason_external(NMDevice *self)
+{
+    NMDeviceStateReason reason;
+
+    /* By default we return reason NOW_MANAGED, which makes the device fully
+     * managed by NM (sys-iface-state=managed). */
+    reason = NM_DEVICE_STATE_REASON_NOW_MANAGED;
+
+    /* If the device is an external-down candidate but no longer has the flag
+     * set, then the device is an externally created interface that previously
+     * had no addresses or no controller and now has.
+     * We need to set CONNECTION_ASSUMED as the reason, so that the device
+     * is managed but is not touched by NM (sys-iface-state=external). */
+    if (nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN)
+        && !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN)) {
+        /* user-udev overwrites external-down, so we only assume the device
+         * when it is a external-down candidate which is not managed via udev. */
+        if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_UDEV)) {
+            reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED;
+        }
+    }
+
+    return reason;
+}
+
 static gboolean
 device_link_changed(gpointer user_data)
 {
@@ -6768,7 +7013,7 @@ device_link_changed(gpointer user_data)
         /* Let any connections that use the new interface name have a chance
          * to auto-activate on the device.
          */
-        nm_device_emit_recheck_auto_activate(self);
+        nm_device_recheck_auto_activate_schedule(self);
     }
 
     if (priv->ipac6_data.ndisc && pllink->inet6_token.id) {
@@ -6793,35 +7038,13 @@ device_link_changed(gpointer user_data)
     priv->up = NM_FLAGS_HAS(pllink->n_ifi_flags, IFF_UP);
 
     if (pllink->initialized && nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT)) {
-        NMDeviceStateReason reason;
-
         nm_device_set_unmanaged_by_user_udev(self);
         nm_device_set_unmanaged_by_user_conf(self);
 
-        reason = NM_DEVICE_STATE_REASON_NOW_MANAGED;
-
-        /* If the device is a external-down candidated but no longer has external
-         * down set, we must clear the platform-unmanaged flag with reason
-         * "assumed". */
-        if (nm_device_get_unmanaged_mask(self, NM_UNMANAGED_EXTERNAL_DOWN)
-            && !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_EXTERNAL_DOWN)) {
-            /* actually, user-udev overwrites external-down. So we only assume the device,
-             * when it is a external-down candidate, which is not managed via udev. */
-            if (!nm_device_get_unmanaged_mask(self, NM_UNMANAGED_USER_UDEV)) {
-                /* Ensure the assume check is queued before any queued state changes
-                 * from the transition to UNAVAILABLE.
-                 */
-                reason = NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED;
-            }
-        }
-
-        /* The assume check should happen before the device transitions to
-         * UNAVAILABLE, because in UNAVAILABLE we already clean up the IP
-         * configuration. Therefore, this function should never trigger a
-         * sync state transition.
-         */
-        nm_device_queue_recheck_assume(self);
-        nm_device_set_unmanaged_by_flags_queue(self, NM_UNMANAGED_PLATFORM_INIT, FALSE, reason);
+        nm_device_set_unmanaged_by_flags_queue(self,
+                                               NM_UNMANAGED_PLATFORM_INIT,
+                                               NM_UNMAN_FLAG_OP_SET_MANAGED,
+                                               nm_device_get_manage_reason_external(self));
     }
 
     _dev_unmanaged_check_external_down(self, FALSE, FALSE);
@@ -7309,14 +7532,15 @@ device_init_static_sriov_num_vfs(NMDevice *self)
     if (priv->ifindex > 0 && nm_device_has_capability(self, NM_DEVICE_CAP_SRIOV)) {
         int num_vfs;
 
-        num_vfs = nm_config_data_get_device_config_int64(NM_CONFIG_GET_DATA,
-                                                         NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
-                                                         self,
-                                                         10,
-                                                         0,
-                                                         G_MAXINT32,
-                                                         -1,
-                                                         -1);
+        num_vfs = nm_config_data_get_device_config_int64_by_device(
+            NM_CONFIG_GET_DATA,
+            NM_CONFIG_KEYFILE_KEY_DEVICE_SRIOV_NUM_VFS,
+            self,
+            10,
+            0,
+            G_MAXINT32,
+            -1,
+            -1);
         if (num_vfs >= 0)
             sriov_op_queue(self, num_vfs, NM_OPTION_BOOL_DEFAULT, NULL, NULL);
     }
@@ -7332,7 +7556,7 @@ config_changed(NMConfig           *config,
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
     if (priv->state <= NM_DEVICE_STATE_DISCONNECTED || priv->state >= NM_DEVICE_STATE_ACTIVATED) {
-        priv->ignore_carrier = nm_config_data_get_ignore_carrier(config_data, self);
+        priv->ignore_carrier = nm_config_data_get_ignore_carrier_by_device(config_data, self);
         if (NM_FLAGS_HAS(changes, NM_CONFIG_CHANGE_VALUES)
             && !nm_device_get_applied_setting(self, NM_TYPE_SETTING_SRIOV))
             device_init_static_sriov_num_vfs(self);
@@ -7471,8 +7695,9 @@ realize_start_setup(NMDevice             *self,
     nm_device_update_permanent_hw_address(self, FALSE);
 
     /* Note: initial hardware address must be read before calling get_ignore_carrier() */
-    config               = nm_config_get();
-    priv->ignore_carrier = nm_config_data_get_ignore_carrier(nm_config_get_data(config), self);
+    config = nm_config_get();
+    priv->ignore_carrier =
+        nm_config_data_get_ignore_carrier_by_device(nm_config_get_data(config), self);
     if (!priv->config_changed_id) {
         priv->config_changed_id = g_signal_connect(config,
                                                    NM_CONFIG_SIGNAL_CONFIG_CHANGED,
@@ -7701,6 +7926,10 @@ nm_device_unrealize(NMDevice *self, gboolean remove_resources, GError **error)
     /* Garbage-collect unneeded unrealized devices. */
     nm_device_recheck_available_connections(self);
 
+    /* In case the unrealized device is not going away, it may need to
+     * autoactivate.  Schedule also a check for that. */
+    nm_device_recheck_auto_activate_schedule(self);
+
     return TRUE;
 }
 
@@ -7721,7 +7950,7 @@ nm_device_notify_availability_maybe_changed(NMDevice *self)
      * available. */
     nm_device_recheck_available_connections(self);
     if (g_hash_table_size(priv->available_connections) > 0)
-        nm_device_emit_recheck_auto_activate(self);
+        nm_device_recheck_auto_activate_schedule(self);
 }
 
 /**
@@ -7880,7 +8109,7 @@ nm_device_master_add_slave(NMDevice *self, NMDevice *slave, gboolean configure)
         g_warn_if_fail(!NM_FLAGS_HAS(slave_priv->unmanaged_mask, NM_UNMANAGED_IS_SLAVE));
         nm_device_set_unmanaged_by_flags(slave,
                                          NM_UNMANAGED_IS_SLAVE,
-                                         FALSE,
+                                         NM_UNMAN_FLAG_OP_SET_MANAGED,
                                          NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
         changed = TRUE;
     } else
@@ -8331,7 +8560,7 @@ nm_device_autoconnect_allowed(NMDevice *self)
             return FALSE;
     }
 
-    if (priv->delete_on_deactivate_data)
+    if (priv->delete_on_deactivate_idle_source)
         return FALSE;
 
     /* The 'autoconnect-allowed' signal is emitted on a device to allow
@@ -8450,7 +8679,7 @@ device_has_config(NMDevice *self)
  * @self: the master #NMDevice
  * @slave: the slave #NMDevice
  * @connection: the #NMConnection to update with the slave settings
- * @GError: (out): error description
+ * @error: error description
  *
  * Reads the slave configuration for @slave and updates @connection with those
  * properties. This invokes a virtual function on the master device @self.
@@ -8719,7 +8948,7 @@ nm_device_complete_connection(NMDevice            *self,
     if (!nm_connection_normalize(connection, NULL, NULL, error))
         return FALSE;
 
-    return nm_device_check_connection_compatible(self, connection, error);
+    return nm_device_check_connection_compatible(self, connection, TRUE, error);
 }
 
 gboolean
@@ -8779,7 +9008,10 @@ nm_device_match_parent_hwaddr(NMDevice     *device,
 }
 
 static gboolean
-check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *self,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDevicePrivate      *priv         = NM_DEVICE_GET_PRIVATE(self);
     const char           *device_iface = nm_device_get_iface(self);
@@ -8906,12 +9138,18 @@ check_connection_compatible(NMDevice *self, NMConnection *connection, GError **e
  *   @self.
  */
 gboolean
-nm_device_check_connection_compatible(NMDevice *self, NMConnection *connection, GError **error)
+nm_device_check_connection_compatible(NMDevice     *self,
+                                      NMConnection *connection,
+                                      gboolean      check_properties,
+                                      GError      **error)
 {
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
     g_return_val_if_fail(NM_IS_CONNECTION(connection), FALSE);
 
-    return NM_DEVICE_GET_CLASS(self)->check_connection_compatible(self, connection, error);
+    return NM_DEVICE_GET_CLASS(self)->check_connection_compatible(self,
+                                                                  connection,
+                                                                  check_properties,
+                                                                  error);
 }
 
 gboolean
@@ -9077,9 +9315,9 @@ nm_device_queue_recheck_available(NMDevice           *self,
 }
 
 void
-nm_device_emit_recheck_auto_activate(NMDevice *self)
+nm_device_recheck_auto_activate_schedule(NMDevice *self)
 {
-    g_signal_emit(self, signals[RECHECK_AUTO_ACTIVATE], 0);
+    nm_manager_device_recheck_auto_activate_schedule(nm_device_get_manager(self), self);
 }
 
 void
@@ -9352,11 +9590,7 @@ sriov_params_cb(GError *error, gpointer user_data)
     if (!nm_platform_link_set_sriov_vfs(nm_device_get_platform(self),
                                         priv->ifindex,
                                         (const NMPlatformVF *const *) plat_vfs)) {
-        _LOGE(LOGD_DEVICE, "failed to apply SR-IOV VFs");
-        nm_device_state_changed(self,
-                                NM_DEVICE_STATE_FAILED,
-                                NM_DEVICE_STATE_REASON_SRIOV_CONFIGURATION_FAILED);
-        return;
+        _LOGW(LOGD_DEVICE, "failed to apply SR-IOV VF configurations");
     }
 
     priv->stage1_sriov_state = NM_DEVICE_STAGE_STATE_COMPLETED;
@@ -9757,10 +9991,12 @@ activate_stage2_device_config(NMDevice *self)
 
     nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
 
-    if (!nm_device_sys_iface_state_is_external_or_assume(self))
+    if (!nm_device_sys_iface_state_is_external(self)) {
         _ethtool_state_set(self);
+        nm_device_link_properties_set(self, FALSE);
+    }
 
-    if (!nm_device_sys_iface_state_is_external_or_assume(self)) {
+    if (!nm_device_sys_iface_state_is_external(self)) {
         if (!priv->tc_committed && !tc_commit(self)) {
             _LOGW(LOGD_DEVICE, "failed applying traffic control rules");
             nm_device_state_changed(self,
@@ -10161,14 +10397,6 @@ _dev_ipmanual_check_ready(NMDevice *self)
             _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_FAILED);
             _dev_ip_state_check_async(self, AF_UNSPEC);
         } else if (ready) {
-            if (priv->ipmanual_data.state_x[IS_IPv4] != NM_DEVICE_IP_STATE_READY
-                && nm_l3cfg_has_temp_not_available_obj(priv->l3cfg, addr_family)) {
-                /* Addresses with pending ACD/DAD are a possible cause for the
-                 * presence of temporarily-not-available objects. Once all addresses
-                 * are ready, retry to commit those unavailable objects. */
-                _dev_l3_cfg_commit(self, FALSE);
-            }
-
             _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_READY);
             _dev_ip_state_check_async(self, AF_UNSPEC);
         }
@@ -10334,7 +10562,6 @@ _dev_ipdhcpx_notify(NMDhcpClient *client, const NMDhcpClientNotifyData *notify_d
         _dev_l3_register_l3cds_set_one_full(self,
                                             L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4),
                                             notify_data->lease_update.l3cd,
-                                            NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE,
                                             FALSE);
 
         if (notify_data->lease_update.accepted) {
@@ -10471,6 +10698,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
             .addr_family             = AF_INET,
             .l3cfg                   = nm_device_get_l3cfg(self),
             .iface                   = nm_device_get_ip_iface(self),
+            .iface_type_log          = nm_device_get_type_desc_for_log(self),
             .uuid                    = nm_connection_get_uuid(connection),
             .hwaddr                  = hwaddr,
             .bcast_hwaddr            = bcast_hwaddr,
@@ -10499,6 +10727,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
         gboolean               iaid_explicit;
         guint32                iaid;
         NMDhcpClientConfig     config;
+        const char            *pd_hint;
 
         iaid = _prop_get_ipvx_dhcp_iaid(self, AF_INET6, connection, FALSE, &iaid_explicit);
         duid = _prop_get_ipv6_dhcp_duid(self, connection, hwaddr, &enforce_duid);
@@ -10507,6 +10736,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
             .addr_family     = AF_INET6,
             .l3cfg           = nm_device_get_l3cfg(self),
             .iface           = nm_device_get_ip_iface(self),
+            .iface_type_log  = nm_device_get_type_desc_for_log(self),
             .uuid            = nm_connection_get_uuid(connection),
             .send_hostname   = nm_setting_ip_config_get_dhcp_send_hostname(s_ip),
             .hostname        = nm_setting_ip_config_get_dhcp_hostname(s_ip),
@@ -10525,6 +10755,21 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                 },
         };
 
+        pd_hint = nm_setting_ip6_config_get_dhcp_pd_hint(NM_SETTING_IP6_CONFIG(s_ip));
+        if (pd_hint) {
+            int      pd_hint_length;
+            gboolean res;
+
+            res = nm_inet_parse_with_prefix_bin(AF_INET6,
+                                                pd_hint,
+                                                NULL,
+                                                &config.v6.pd_hint_addr,
+                                                &pd_hint_length);
+            nm_assert(res);
+            nm_assert(pd_hint_length > 0 && pd_hint_length <= 128);
+            config.v6.pd_hint_length = pd_hint_length;
+        }
+
         priv->ipdhcp_data_6.client =
             nm_dhcp_manager_start_client(nm_dhcp_manager_get(), &config, &error);
     }
@@ -10555,7 +10800,6 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
         _dev_l3_register_l3cds_set_one_full(self,
                                             L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4),
                                             previous_lease,
-                                            NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE,
                                             FALSE);
     }
 
@@ -10680,10 +10924,13 @@ connection_ip_method_requires_carrier(NMConnection *connection,
 static gboolean
 connection_requires_carrier(NMConnection *connection)
 {
-    NMSettingIPConfig   *s_ip4, *s_ip6;
+    NMSettingIPConfig   *s_ip4;
+    NMSettingIPConfig   *s_ip6;
     NMSettingConnection *s_con;
-    gboolean             ip4_carrier_wanted, ip6_carrier_wanted;
-    gboolean             ip4_used = FALSE, ip6_used = FALSE;
+    gboolean             ip4_carrier_wanted;
+    gboolean             ip6_carrier_wanted;
+    gboolean             ip4_used = FALSE;
+    gboolean             ip6_used = FALSE;
 
     /* We can progress to IP_CONFIG now, so that we're enslaved.
      * That may actually cause carrier to go up and thus continue activation. */
@@ -11196,10 +11443,8 @@ _commit_mtu(NMDevice *self)
     if (ifindex <= 0)
         return;
 
-    if (!nm_device_get_applied_connection(self)
-        || nm_device_sys_iface_state_is_external_or_assume(self)) {
-        /* we don't tamper with the MTU of disconnected and
-         * external/assumed devices. */
+    if (!nm_device_get_applied_connection(self) || nm_device_sys_iface_state_is_external(self)) {
+        /* we don't tamper with the MTU of disconnected and external devices. */
         return;
     }
 
@@ -11378,8 +11623,8 @@ _commit_mtu(NMDevice *self)
                                  ? "Are the MTU sizes of the slaves large enough?"
                                  : "Did you configure the MTU correctly?"));
             }
-            priv->carrier_wait_until_ms =
-                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+            priv->carrier_wait_until_msec =
+                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MSEC;
         }
 
         if (ip6_mtu && ip6_mtu != _IP6_MTU_SYS()) {
@@ -11408,8 +11653,8 @@ _commit_mtu(NMDevice *self)
                        msg ? ": " : "",
                        msg ?: "");
             }
-            priv->carrier_wait_until_ms =
-                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+            priv->carrier_wait_until_msec =
+                nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MSEC;
         }
     }
 
@@ -11488,11 +11733,7 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
 
     _dev_ipac6_grace_period_start(self, 0, TRUE);
 
-    _dev_l3_register_l3cds_set_one_full(self,
-                                        L3_CONFIG_DATA_TYPE_AC_6,
-                                        l3cd,
-                                        NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE,
-                                        FALSE);
+    _dev_l3_register_l3cds_set_one_full(self, L3_CONFIG_DATA_TYPE_AC_6, l3cd, FALSE);
 
     nm_clear_l3cd(&priv->ipac6_data.l3cd);
     ready = nm_l3cfg_check_ready(priv->l3cfg,
@@ -11682,7 +11923,11 @@ _dev_ipac6_start(NMDevice *self)
     }
 
     if (nm_device_get_ip_iface_identifier(self, &iid, FALSE, &is_token)) {
-        _LOGD_ipac6("using the device EUI-64 identifier");
+        char buf[INET6_ADDRSTRLEN];
+
+        _LOGD_ipac6("using the device EUI-64 identifier %s (from %s)",
+                    nm_utils_inet6_interface_identifier_to_token(&iid, buf),
+                    is_token ? "token" : "address");
         nm_ndisc_set_iid(priv->ipac6_data.ndisc, iid, is_token);
     } else {
         /* Don't abort the addrconf at this point -- if ndisc needs the iid
@@ -12610,24 +12855,24 @@ nm_device_is_nm_owned(NMDevice *self)
 static gboolean
 delete_on_deactivate_link_delete(gpointer user_data)
 {
-    DeleteOnDeactivateData        *data  = user_data;
-    nm_auto_unref_object NMDevice *self  = data->device;
+    nm_auto_unref_object NMDevice *self  = user_data;
     NMDevicePrivate               *priv  = NM_DEVICE_GET_PRIVATE(self);
     gs_free_error GError          *error = NULL;
 
-    _LOGD(LOGD_DEVICE,
-          "delete_on_deactivate: cleanup and delete virtual link (id=%u)",
-          data->idle_add_id);
+    _LOGD(LOGD_DEVICE, "delete_on_deactivate: cleanup and delete virtual link");
 
-    priv->delete_on_deactivate_data = NULL;
+    nm_clear_g_source_inst(&priv->delete_on_deactivate_idle_source);
 
     if (!nm_device_unrealize(self, TRUE, &error))
         _LOGD(LOGD_DEVICE, "delete_on_deactivate: unrealizing failed (%s)", error->message);
 
-    nm_device_emit_recheck_auto_activate(self);
+    if (nm_dbus_object_is_exported(NM_DBUS_OBJECT(self))) {
+        /* The device is still alive. We may need to autoactivate virtual
+         * devices again. */
+        nm_device_recheck_auto_activate_schedule(self);
+    }
 
-    g_free(data);
-    return FALSE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
@@ -12635,25 +12880,16 @@ delete_on_deactivate_unschedule(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    if (priv->delete_on_deactivate_data) {
-        DeleteOnDeactivateData *data = priv->delete_on_deactivate_data;
-
-        priv->delete_on_deactivate_data = NULL;
-
-        g_source_remove(data->idle_add_id);
-        _LOGD(LOGD_DEVICE,
-              "delete_on_deactivate: cancel cleanup and delete virtual link (id=%u)",
-              data->idle_add_id);
-        g_object_unref(data->device);
-        g_free(data);
+    if (nm_clear_g_source_inst(&priv->delete_on_deactivate_idle_source)) {
+        _LOGD(LOGD_DEVICE, "delete_on_deactivate: cancel cleanup and delete virtual link");
+        g_object_unref(self);
     }
 }
 
 static void
 delete_on_deactivate_check_and_schedule(NMDevice *self)
 {
-    NMDevicePrivate        *priv = NM_DEVICE_GET_PRIVATE(self);
-    DeleteOnDeactivateData *data;
+    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
     if (!priv->nm_owned)
         return;
@@ -12663,18 +12899,13 @@ delete_on_deactivate_check_and_schedule(NMDevice *self)
         return;
     if (nm_device_get_state(self) == NM_DEVICE_STATE_UNMANAGED)
         return;
-    if (nm_device_get_state(self) == NM_DEVICE_STATE_UNAVAILABLE)
-        return;
-    delete_on_deactivate_unschedule(self); /* always cancel and reschedule */
 
-    data                            = g_new(DeleteOnDeactivateData, 1);
-    data->device                    = g_object_ref(self);
-    data->idle_add_id               = g_idle_add(delete_on_deactivate_link_delete, data);
-    priv->delete_on_deactivate_data = data;
+    g_object_ref(self);
+    delete_on_deactivate_unschedule(self); /* always cancel and reschedule */
+    priv->delete_on_deactivate_idle_source =
+        nm_g_idle_add_source(delete_on_deactivate_link_delete, self);
 
-    _LOGD(LOGD_DEVICE,
-          "delete_on_deactivate: schedule cleanup and delete virtual link (id=%u)",
-          data->idle_add_id);
+    _LOGD(LOGD_DEVICE, "delete_on_deactivate: schedule cleanup and delete virtual link");
 }
 
 static void
@@ -12835,7 +13066,8 @@ can_reapply_change(NMDevice   *self,
                      NM_SETTING_USER_SETTING_NAME,
                      NM_SETTING_PROXY_SETTING_NAME,
                      NM_SETTING_IP4_CONFIG_SETTING_NAME,
-                     NM_SETTING_IP6_CONFIG_SETTING_NAME))
+                     NM_SETTING_IP6_CONFIG_SETTING_NAME,
+                     NM_SETTING_LINK_SETTING_NAME))
         return TRUE;
 
     if (nm_streq(setting_name, NM_SETTING_WIRED_SETTING_NAME)) {
@@ -12884,7 +13116,7 @@ reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new)
  * Change configuration of an already configured device if possible.
  * Updates the device's applied connection upon success.
  *
- * Return: %FALSE if the new configuration can not be reapplied.
+ * Returns: %FALSE if the new configuration can not be reapplied.
  */
 static gboolean
 check_and_reapply_connection(NMDevice            *self,
@@ -13032,6 +13264,8 @@ check_and_reapply_connection(NMDevice            *self,
      *************************************************************************/
     klass->reapply_connection(self, con_old, con_new);
 
+    nm_device_link_properties_set(self, TRUE);
+
     if (priv->state >= NM_DEVICE_STATE_CONFIG)
         lldp_setup(self, NM_TERNARY_DEFAULT);
 
@@ -13079,7 +13313,7 @@ check_and_reapply_connection(NMDevice            *self,
     if (sett_conn) {
         nm_settings_connection_autoconnect_blocked_reason_set(
             sett_conn,
-            NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST,
             FALSE);
     }
 
@@ -13414,7 +13648,8 @@ delete_cb(NMDevice              *self,
           GError                *error,
           gpointer               user_data)
 {
-    GError *local = NULL;
+    NMSettingsConnection *sett_conn;
+    GError               *local = NULL;
 
     if (error) {
         g_dbus_method_invocation_return_gerror(context, error);
@@ -13429,10 +13664,26 @@ delete_cb(NMDevice              *self,
 
     /* Authorized */
     nm_audit_log_device_op(NM_AUDIT_OP_DEVICE_DELETE, self, TRUE, NULL, subject, NULL);
-    if (nm_device_unrealize(self, TRUE, &local))
-        g_dbus_method_invocation_return_value(context, NULL);
-    else
+
+    sett_conn = nm_device_get_settings_connection(self);
+    if (sett_conn) {
+        /* Block profile from autoconnecting. We block the profile, which may
+         * be ugly/wrong with multi-connect profiles. However, it's not
+         * obviously wrong, because profiles for software devices tend not to
+         * work with multi-connect anyway, because they describe a (unique)
+         * interface by name. */
+        nm_settings_connection_autoconnect_blocked_reason_set(
+            sett_conn,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST,
+            TRUE);
+    }
+
+    if (!nm_device_unrealize(self, TRUE, &local)) {
         g_dbus_method_invocation_take_error(context, local);
+        return;
+    }
+
+    g_dbus_method_invocation_return_value(context, NULL);
 }
 
 static void
@@ -13539,7 +13790,7 @@ _carrier_wait_check_act_request_must_queue(NMDevice *self, NMActRequest *req)
      * request is not blocked waiting for carrier. */
     if (priv->carrier)
         return FALSE;
-    if (priv->carrier_wait_id == 0)
+    if (!priv->carrier_wait_source)
         return FALSE;
 
     connection = nm_act_request_get_applied_connection(req);
@@ -14053,11 +14304,11 @@ carrier_wait_timeout(gpointer user_data)
     NMDevice        *self = NM_DEVICE(user_data);
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    priv->carrier_wait_id = 0;
+    nm_clear_g_source_inst(&priv->carrier_wait_source);
     nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
     if (!priv->carrier)
         _carrier_wait_check_queued_act_request(self);
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static gboolean
@@ -14074,14 +14325,15 @@ nm_device_is_up(NMDevice *self)
 static gint64
 _get_carrier_wait_ms(NMDevice *self)
 {
-    return nm_config_data_get_device_config_int64(NM_CONFIG_GET_DATA,
-                                                  NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT,
-                                                  self,
-                                                  10,
-                                                  0,
-                                                  G_MAXINT32,
-                                                  CARRIER_WAIT_TIME_MS,
-                                                  CARRIER_WAIT_TIME_MS);
+    return nm_config_data_get_device_config_int64_by_device(
+        NM_CONFIG_GET_DATA,
+        NM_CONFIG_KEYFILE_KEY_DEVICE_CARRIER_WAIT_TIMEOUT,
+        self,
+        10,
+        0,
+        G_MAXINT32,
+        CARRIER_WAIT_TIME_MS,
+        CARRIER_WAIT_TIME_MS);
 }
 
 /*
@@ -14104,13 +14356,14 @@ carrier_detect_wait(NMDevice *self)
      *
      * If during that time carrier goes away, we declare the interface
      * as not ready. */
-    nm_clear_g_source(&priv->carrier_wait_id);
+    nm_clear_g_source_inst(&priv->carrier_wait_source);
     if (!priv->carrier)
         nm_device_add_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
 
     now_ms   = nm_utils_get_monotonic_timestamp_msec();
-    until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_ms);
-    priv->carrier_wait_id = g_timeout_add(until_ms - now_ms, carrier_wait_timeout, self);
+    until_ms = NM_MAX(now_ms + _get_carrier_wait_ms(self), priv->carrier_wait_until_msec);
+    priv->carrier_wait_source =
+        nm_g_timeout_add_source(until_ms - now_ms, carrier_wait_timeout, self);
 }
 
 gboolean
@@ -14563,7 +14816,15 @@ _set_unmanaged_flags(NMDevice           *self,
         new_state = was_managed ? NM_DEVICE_STATE_UNMANAGED : NM_DEVICE_STATE_UNAVAILABLE;
         if (new_state == NM_DEVICE_STATE_UNMANAGED) {
             _cancel_activation(self);
+        } else {
+            /* The assume check should happen before the device transitions to
+            * UNAVAILABLE, because in UNAVAILABLE we already clean up the IP
+            * configuration. Therefore, this function should never trigger a
+            * sync state transition.
+            */
+            nm_device_queue_recheck_assume(self);
         }
+
         if (now)
             nm_device_state_changed(self, new_state, reason);
         else
@@ -14629,11 +14890,11 @@ nm_device_check_unrealized_device_managed(NMDevice *self)
 
     nm_assert(!nm_device_is_real(self));
 
-    if (!nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
-                                                  NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
-                                                  self,
-                                                  TRUE,
-                                                  TRUE))
+    if (!nm_config_data_get_device_config_boolean_by_device(NM_CONFIG_GET_DATA,
+                                                            NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                            self,
+                                                            TRUE,
+                                                            TRUE))
         return FALSE;
 
     if (nm_device_spec_match_list(self, nm_settings_get_unmanaged_specs(priv->settings)))
@@ -14700,11 +14961,11 @@ nm_device_set_unmanaged_by_user_conf(NMDevice *self)
     gboolean      value;
     NMUnmanFlagOp set_op;
 
-    value = nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
-                                                     NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
-                                                     self,
-                                                     -1,
-                                                     TRUE);
+    value = nm_config_data_get_device_config_boolean_by_device(NM_CONFIG_GET_DATA,
+                                                               NM_CONFIG_KEYFILE_KEY_DEVICE_MANAGED,
+                                                               self,
+                                                               -1,
+                                                               TRUE);
     switch (value) {
     case TRUE:
         set_op = NM_UNMAN_FLAG_OP_SET_MANAGED;
@@ -14739,7 +15000,7 @@ nm_device_set_unmanaged_by_quitting(NMDevice *self)
 
     nm_device_set_unmanaged_by_flags(self,
                                      NM_UNMANAGED_QUITTING,
-                                     TRUE,
+                                     NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                      need_deactivate ? NM_DEVICE_STATE_REASON_REMOVED
                                                      : NM_DEVICE_STATE_REASON_NOW_UNMANAGED);
 }
@@ -14918,7 +15179,10 @@ _nm_device_check_connection_available(NMDevice                      *self,
     /* an unrealized software device is always available, hardware devices never. */
     if (!nm_device_is_real(self)) {
         if (nm_device_is_software(self)) {
-            if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) {
+            if (!nm_device_check_connection_compatible(self,
+                                                       connection,
+                                                       TRUE,
+                                                       error ? &local : NULL)) {
                 if (error) {
                     g_return_val_if_fail(local, FALSE);
                     nm_utils_error_set(error,
@@ -14982,7 +15246,7 @@ _nm_device_check_connection_available(NMDevice                      *self,
         }
     }
 
-    if (!nm_device_check_connection_compatible(self, connection, error ? &local : NULL)) {
+    if (!nm_device_check_connection_compatible(self, connection, TRUE, error ? &local : NULL)) {
         if (error) {
             nm_utils_error_set(error,
                                local->domain == NM_UTILS_ERROR ? local->code
@@ -15084,14 +15348,11 @@ check_connection_available(NMDevice                      *self,
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
-    /* Connections which require a network connection are not available when
-     * the device has no carrier, even with ignore-carrer=TRUE.
-     */
-    if (priv->carrier || !connection_requires_carrier(connection))
+    if (priv->carrier)
         return TRUE;
 
     if (NM_FLAGS_HAS(flags, _NM_DEVICE_CHECK_CON_AVAILABLE_FOR_USER_REQUEST_WAITING_CARRIER)
-        && priv->carrier_wait_id != 0) {
+        && priv->carrier_wait_source) {
         /* The device has no carrier though the connection requires it.
          *
          * If we are still waiting for carrier, the connection is available
@@ -15099,12 +15360,6 @@ check_connection_available(NMDevice                      *self,
         return TRUE;
     }
 
-    /* master types are always available even without carrier.
-     * Making connection non-available would un-enslave slaves which
-     * is not desired. */
-    if (nm_device_is_master(self))
-        return TRUE;
-
     if (!priv->up) {
         /* If the device is !IFF_UP it also has no carrier. But we assume that if we
          * would start activating the device (and thereby set the device IFF_UP),
@@ -15114,6 +15369,18 @@ check_connection_available(NMDevice                      *self,
         return TRUE;
     }
 
+    if (!connection_requires_carrier(connection)) {
+        /* Connections that don't require carrier are available. */
+        return TRUE;
+    }
+
+    if (nm_device_is_master(self)) {
+        /* master types are always available even without carrier.
+         * Making connection non-available would un-enslave slaves which
+         * is not desired. */
+        return TRUE;
+    }
+
     nm_utils_error_set_literal(error,
                                NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                "device has no carrier");
@@ -15479,7 +15746,7 @@ _cleanup_generic_pre(NMDevice *self, CleanupType cleanup_type)
 }
 
 static void
-_cleanup_generic_post(NMDevice *self, CleanupType cleanup_type)
+_cleanup_generic_post(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanup_type)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
@@ -15502,7 +15769,11 @@ _cleanup_generic_post(NMDevice *self, CleanupType cleanup_type)
         act_request_set(self, NULL);
     }
 
-    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE
+        && ((reason == NM_DEVICE_STATE_REASON_CARRIER && nm_device_is_master(self))
+            || !NM_IN_SET(reason,
+                          NM_DEVICE_STATE_REASON_NOW_MANAGED,
+                          NM_DEVICE_STATE_REASON_CARRIER))) {
         /* Check if the device was deactivated, and if so, delete_link.
          * Don't call delete_link synchronously because we are currently
          * handling a state change -- which is not reentrant. */
@@ -15623,8 +15894,8 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
                   ifindex);
             if (priv->mtu_initial) {
                 nm_platform_link_set_mtu(nm_device_get_platform(self), ifindex, priv->mtu_initial);
-                priv->carrier_wait_until_ms =
-                    nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MS;
+                priv->carrier_wait_until_msec =
+                    nm_utils_get_monotonic_timestamp_msec() + CARRIER_WAIT_TIME_AFTER_MTU_MSEC;
             }
             if (priv->ip6_mtu_initial) {
                 char sbuf[64];
@@ -15641,6 +15912,7 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
     }
 
     _ethtool_state_reset(self);
+    link_properties_reset(self);
 
     if (priv->promisc_reset != NM_OPTION_BOOL_DEFAULT && ifindex > 0) {
         nm_platform_link_change_flags(nm_device_get_platform(self),
@@ -15650,7 +15922,7 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
         priv->promisc_reset = NM_OPTION_BOOL_DEFAULT;
     }
 
-    _cleanup_generic_post(self, cleanup_type);
+    _cleanup_generic_post(self, reason, cleanup_type);
 }
 
 static void
@@ -15675,6 +15947,9 @@ deactivate_ready(NMDevice *self, NMDeviceStateReason reason)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
 
+    if (priv->port_detach_count > 0)
+        return;
+
     if (priv->dispatcher.call_id)
         return;
 
@@ -15966,8 +16241,11 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
              * userspace IPv6LL enabled.
              */
             _dev_addrgenmode6_set(self, NM_IN6_ADDR_GEN_MODE_NONE);
+            if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_REMOVED) {
+                nm_device_cleanup(self, reason, CLEANUP_TYPE_REMOVED);
+            } else
+                nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
 
-            nm_device_cleanup(self, reason, CLEANUP_TYPE_DECONFIGURE);
         } else if (old_state < NM_DEVICE_STATE_DISCONNECTED) {
             if (priv->sys_iface_state == NM_DEVICE_SYS_IFACE_STATE_MANAGED) {
                 /* Ensure IPv6 is set up as it may not have been done when
@@ -16036,7 +16314,8 @@ _set_state_full(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason,
 
         /* We cache the ignore_carrier state to not react on config-reloads while the connection
          * is active. But on deactivating, reset the ignore-carrier flag to the current state. */
-        priv->ignore_carrier = nm_config_data_get_ignore_carrier(NM_CONFIG_GET_DATA, self);
+        priv->ignore_carrier =
+            nm_config_data_get_ignore_carrier_by_device(NM_CONFIG_GET_DATA, self);
 
         if (quitting) {
             nm_dispatcher_call_device_sync(NM_DISPATCHER_ACTION_PRE_DOWN, self, req);
@@ -16743,7 +17022,7 @@ nm_device_hw_addr_set(NMDevice *self, const char *addr, const char *detail, gboo
  * @hwaddr: (out): the cloned MAC address to set on interface
  * @hwaddr_type: (out): the type of address to set
  * @hwaddr_detail: (out): the detail (origin) of address to set
- * @error: (out): on return, an error or %NULL
+ * @error: on return, an error or %NULL
  *
  * Computes the MAC to be set on a interface. On success, one of the
  * following exclusive conditions are verified:
@@ -16940,6 +17219,7 @@ nm_device_hw_addr_reset(NMDevice *self, const char *detail)
 {
     NMDevicePrivate *priv;
     const char      *addr;
+    int              ifindex;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
 
@@ -16949,7 +17229,13 @@ nm_device_hw_addr_reset(NMDevice *self, const char *detail)
         return TRUE;
 
     priv->hw_addr_type = HW_ADDR_TYPE_UNSET;
-    addr               = nm_device_get_initial_hw_address(self);
+
+    ifindex = nm_device_get_ip_ifindex(self);
+    if (ifindex <= 0) {
+        return TRUE;
+    }
+
+    addr = nm_device_get_initial_hw_address(self);
     if (!addr) {
         /* as hw_addr_type is not UNSET, we expect that we can get an
          * initial address to which to reset. */
@@ -17028,38 +17314,11 @@ nm_device_spec_match_list(NMDevice *self, const GSList *specs)
 int
 nm_device_spec_match_list_full(NMDevice *self, const GSList *specs, int no_match_value)
 {
-    NMDeviceClass       *klass;
-    NMMatchSpecMatchType m;
-    const char          *hw_address = NULL;
-    gboolean             is_fake;
-
-    g_return_val_if_fail(NM_IS_DEVICE(self), FALSE);
+    NMMatchSpecDeviceData data;
+    NMMatchSpecMatchType  m;
 
-    klass      = NM_DEVICE_GET_CLASS(self);
-    hw_address = nm_device_get_permanent_hw_address_full(
-        self,
-        !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_PLATFORM_INIT),
-        &is_fake);
-
-    m = nm_match_spec_device(specs,
-                             nm_device_get_iface(self),
-                             nm_device_get_type_description(self),
-                             nm_device_get_driver(self),
-                             nm_device_get_driver_version(self),
-                             is_fake ? NULL : hw_address,
-                             klass->get_s390_subchannels ? klass->get_s390_subchannels(self) : NULL,
-                             nm_dhcp_manager_get_config(nm_dhcp_manager_get()));
-
-    switch (m) {
-    case NM_MATCH_SPEC_MATCH:
-        return TRUE;
-    case NM_MATCH_SPEC_NEG_MATCH:
-        return FALSE;
-    case NM_MATCH_SPEC_NO_MATCH:
-        return no_match_value;
-    }
-    nm_assert_not_reached();
-    return no_match_value;
+    m = nm_match_spec_device(specs, nm_match_spec_device_data_init_from_device(&data, self));
+    return nm_match_spec_match_type_to_bool(m, no_match_value);
 }
 
 guint
@@ -17747,6 +18006,8 @@ nm_device_init(NMDevice *self)
 
     c_list_init(&priv->concheck_lst_head);
     c_list_init(&self->devices_lst);
+    c_list_init(&self->devcon_dev_lst_head);
+    c_list_init(&self->policy_auto_activate_lst);
     c_list_init(&priv->slaves);
 
     priv->ipdhcp_data_6.v6.mode = NM_NDISC_DHCP_LEVEL_NONE;
@@ -17773,7 +18034,11 @@ nm_device_init(NMDevice *self)
     priv->unmanaged_mask        = priv->unmanaged_flags;
     priv->available_connections = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL);
     priv->ip6_saved_properties  = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);
-    priv->sys_iface_state_      = NM_DEVICE_SYS_IFACE_STATE_EXTERNAL;
+
+    priv->sys_iface_state_ = NM_DEVICE_SYS_IFACE_STATE_EXTERNAL;
+    /* If networking is already disabled at boot, we want to manage all devices
+     * after re-enabling networking; hence, the initial state is MANAGED. */
+    priv->sys_iface_state_before_sleep = NM_DEVICE_SYS_IFACE_STATE_MANAGED;
 
     priv->promisc_reset = NM_OPTION_BOOL_DEFAULT;
 }
@@ -17867,6 +18132,9 @@ dispose(GObject *object)
     _LOGD(LOGD_DEVICE, "disposing");
 
     nm_assert(c_list_is_empty(&self->devices_lst));
+    nm_assert(c_list_is_empty(&self->devcon_dev_lst_head));
+    nm_assert(c_list_is_empty(&self->policy_auto_activate_lst));
+    nm_assert(!self->policy_auto_activate_idle_source);
 
     while ((con_handle = c_list_first_entry(&priv->concheck_lst_head,
                                             NMDeviceConnectivityHandle,
@@ -17899,7 +18167,7 @@ dispose(GObject *object)
     /* Let the kernel manage IPv6LL again */
     _dev_addrgenmode6_set(self, NM_IN6_ADDR_GEN_MODE_EUI64);
 
-    _cleanup_generic_post(self, CLEANUP_TYPE_KEEP);
+    _cleanup_generic_post(self, NM_DEVICE_STATE_REASON_NONE, CLEANUP_TYPE_KEEP);
 
     nm_assert(priv->master_ready_id == 0);
 
@@ -17925,7 +18193,7 @@ dispose(GObject *object)
 
     available_connections_del_all(self);
 
-    if (nm_clear_g_source(&priv->carrier_wait_id))
+    if (nm_clear_g_source_inst(&priv->carrier_wait_source))
         nm_device_remove_pending_action(self, NM_PENDING_ACTION_CARRIER_WAIT, FALSE);
 
     _clear_queued_act_request(priv, NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED);
@@ -18529,16 +18797,6 @@ nm_device_class_init(NMDeviceClass *klass)
                                     G_TYPE_NONE,
                                     0);
 
-    signals[RECHECK_AUTO_ACTIVATE] = g_signal_new(NM_DEVICE_RECHECK_AUTO_ACTIVATE,
-                                                  G_OBJECT_CLASS_TYPE(object_class),
-                                                  G_SIGNAL_RUN_FIRST,
-                                                  0,
-                                                  NULL,
-                                                  NULL,
-                                                  NULL,
-                                                  G_TYPE_NONE,
-                                                  0);
-
     signals[RECHECK_ASSUME] = g_signal_new(NM_DEVICE_RECHECK_ASSUME,
                                            G_OBJECT_CLASS_TYPE(object_class),
                                            G_SIGNAL_RUN_FIRST,
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index bcf4d7b9..b096d23a 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -75,7 +75,6 @@
 #define NM_DEVICE_IP6_PREFIX_DELEGATED     "ip6-prefix-delegated"
 #define NM_DEVICE_IP6_SUBNET_NEEDED        "ip6-subnet-needed"
 #define NM_DEVICE_REMOVED                  "removed"
-#define NM_DEVICE_RECHECK_AUTO_ACTIVATE    "recheck-auto-activate"
 #define NM_DEVICE_RECHECK_ASSUME           "recheck-assume"
 #define NM_DEVICE_STATE_CHANGED            "state-changed"
 #define NM_DEVICE_LINK_INITIALIZED         "link-initialized"
@@ -144,6 +143,10 @@ struct _NMDevice {
     NMDBusObject             parent;
     struct _NMDevicePrivate *_priv;
     CList                    devices_lst;
+    CList                    devcon_dev_lst_head;
+
+    CList    policy_auto_activate_lst;
+    GSource *policy_auto_activate_idle_source;
 };
 
 /* The flags have an relaxing meaning, that means, specifying more flags, can make
@@ -294,7 +297,7 @@ typedef struct _NMDeviceClass {
     GPtrArray *(*get_extra_rules)(NMDevice *self);
 
     /* allow derived classes to override the result of nm_device_autoconnect_allowed().
-     * If the value changes, the class should call nm_device_emit_recheck_auto_activate(),
+     * If the value changes, the class should call nm_device_recheck_auto_activate_schedule(),
      * which emits NM_DEVICE_RECHECK_AUTO_ACTIVATE signal. */
     gboolean (*get_autoconnect_allowed)(NMDevice *self);
 
@@ -321,6 +324,7 @@ typedef struct _NMDeviceClass {
      */
     gboolean (*check_connection_compatible)(NMDevice     *self,
                                             NMConnection *connection,
+                                            gboolean      check_properties,
                                             GError      **error);
 
     /* Checks whether the connection is likely available to be activated,
@@ -387,7 +391,15 @@ typedef struct _NMDeviceClass {
                              GCancellable              *cancellable,
                              NMDeviceAttachPortCallback callback,
                              gpointer                   user_data);
-    void (*detach_port)(NMDevice *self, NMDevice *port, gboolean configure);
+    /* This works similarly to attach_port(). However, current
+     * implementations don't report errors and so the only possible
+     * return values are TRUE and DEFAULT. */
+    NMTernary (*detach_port)(NMDevice                  *self,
+                             NMDevice                  *port,
+                             gboolean                   configure,
+                             GCancellable              *cancellable,
+                             NMDeviceAttachPortCallback callback,
+                             gpointer                   user_data);
 
     void (*parent_changed_notify)(NMDevice *self,
                                   int       old_ifindex,
@@ -421,6 +433,10 @@ typedef struct _NMDeviceClass {
     const char *(*get_dhcp_anycast_address)(NMDevice *self);
 } NMDeviceClass;
 
+NMSettings *nm_device_get_settings(NMDevice *self);
+
+NMManager *nm_device_get_manager(NMDevice *self);
+
 GType nm_device_get_type(void);
 
 struct _NMDedupMultiIndex *nm_device_get_multi_index(NMDevice *self);
@@ -444,9 +460,11 @@ gboolean     nm_device_is_real(NMDevice *dev);
 const char  *nm_device_get_ip_iface(NMDevice *dev);
 const char  *nm_device_get_ip_iface_from_platform(NMDevice *dev);
 int          nm_device_get_ip_ifindex(const NMDevice *dev);
+const char  *nm_device_get_s390_subchannels(NMDevice *self);
 const char  *nm_device_get_driver(NMDevice *dev);
 const char  *nm_device_get_driver_version(NMDevice *dev);
 const char  *nm_device_get_type_desc(NMDevice *dev);
+const char  *nm_device_get_type_desc_for_log(NMDevice *dev);
 const char  *nm_device_get_type_description(NMDevice *dev);
 NMDeviceType nm_device_get_device_type(NMDevice *dev);
 NMLinkType   nm_device_get_link_type(NMDevice *dev);
@@ -526,8 +544,10 @@ gboolean nm_device_complete_connection(NMDevice            *device,
                                        NMConnection *const *existing_connections,
                                        GError             **error);
 
-gboolean
-nm_device_check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error);
+gboolean nm_device_check_connection_compatible(NMDevice     *device,
+                                               NMConnection *connection,
+                                               gboolean      check_properties,
+                                               GError      **error);
 
 gboolean nm_device_check_slave_connection_compatible(NMDevice *device, NMConnection *connection);
 
@@ -608,9 +628,9 @@ typedef enum {
 } NMUnmanagedFlags;
 
 typedef enum {
-    NM_UNMAN_FLAG_OP_SET_MANAGED   = FALSE,
-    NM_UNMAN_FLAG_OP_SET_UNMANAGED = TRUE,
-    NM_UNMAN_FLAG_OP_FORGET        = 2,
+    NM_UNMAN_FLAG_OP_SET_MANAGED = 0,
+    NM_UNMAN_FLAG_OP_SET_UNMANAGED,
+    NM_UNMAN_FLAG_OP_FORGET,
 } NMUnmanFlagOp;
 
 const char *nm_unmanaged_flags2str(NMUnmanagedFlags flags, char *buf, gsize len);
@@ -631,6 +651,7 @@ void nm_device_set_unmanaged_by_user_settings(NMDevice *self, gboolean now);
 void nm_device_set_unmanaged_by_user_udev(NMDevice *self);
 void nm_device_set_unmanaged_by_user_conf(NMDevice *self);
 void nm_device_set_unmanaged_by_quitting(NMDevice *device);
+NMDeviceStateReason nm_device_get_manage_reason_external(NMDevice *self);
 
 gboolean nm_device_check_unrealized_device_managed(NMDevice *self);
 
@@ -701,7 +722,7 @@ nm_device_autoconnect_blocked_unset(NMDevice *device, NMDeviceAutoconnectBlocked
     nm_device_autoconnect_blocked_set_full(device, mask, NM_DEVICE_AUTOCONNECT_BLOCKED_NONE);
 }
 
-void nm_device_emit_recheck_auto_activate(NMDevice *device);
+void nm_device_recheck_auto_activate_schedule(NMDevice *device);
 
 NMDeviceSysIfaceState nm_device_sys_iface_state_get(NMDevice *device);
 
@@ -710,6 +731,10 @@ gboolean nm_device_sys_iface_state_is_external_or_assume(NMDevice *self);
 
 void nm_device_sys_iface_state_set(NMDevice *device, NMDeviceSysIfaceState sys_iface_state);
 
+void nm_device_notify_sleeping(NMDevice *self);
+
+NMDeviceSysIfaceState nm_device_get_sys_iface_state_before_sleep(NMDevice *self);
+
 void nm_device_state_changed(NMDevice *device, NMDeviceState state, NMDeviceStateReason reason);
 
 void nm_device_queue_state(NMDevice *self, NMDeviceState state, NMDeviceStateReason reason);
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index 7b319af3..ff1917b1 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -97,9 +97,16 @@ attach_port(NMDevice                  *device,
     return TRUE;
 }
 
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
-{}
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
+{
+    return TRUE;
+}
 
 void
 nm_device_ovs_reapply_connection(NMDevice *self, NMConnection *con_old, NMConnection *con_new)
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c
index 711f65cb..fd48c2fd 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.c
+++ b/src/core/devices/ovs/nm-device-ovs-interface.c
@@ -91,12 +91,15 @@ can_auto_connect(NMDevice *device, NMSettingsConnection *sett_conn, char **speci
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingOvsInterface *s_ovs_iface;
 
     if (!NM_DEVICE_CLASS(nm_device_ovs_interface_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_ovs_iface = nm_connection_get_setting_ovs_interface(connection);
@@ -132,6 +135,8 @@ link_changed(NMDevice *device, const NMPlatformLink *pllink)
             nm_device_devip_set_failed(device, AF_INET6, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
             return;
         }
+
+        nm_device_link_properties_set(device, FALSE);
         nm_device_bring_up(device);
 
         nm_device_devip_set_state(device, AF_INET, NM_DEVICE_IP_STATE_PENDING, NULL);
@@ -214,6 +219,7 @@ _set_ip_ifindex_tun(gpointer user_data)
 
     priv->wait_link_is_waiting = FALSE;
     nm_device_set_ip_ifindex(device, priv->wait_link_ifindex);
+    nm_device_link_properties_set(device, FALSE);
 
     nm_device_devip_set_state(device, AF_INET, NM_DEVICE_IP_STATE_PENDING, NULL);
     nm_device_devip_set_state(device, AF_INET6, NM_DEVICE_IP_STATE_PENDING, NULL);
@@ -303,6 +309,7 @@ act_stage3_ip_config(NMDevice *device, int addr_family)
         return;
     }
 
+    nm_device_link_properties_set(device, FALSE);
     nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
 }
 
@@ -479,7 +486,7 @@ ovsdb_ready(NMOvsdb *ovsdb, NMDeviceOvsInterface *self)
                                       NM_DEVICE_STATE_REASON_NONE,
                                       NM_DEVICE_STATE_REASON_NONE);
     nm_device_recheck_available_connections(device);
-    nm_device_emit_recheck_auto_activate(device);
+    nm_device_recheck_auto_activate_schedule(device);
 }
 
 static void
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
index 5510e39f..5ede46e9 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.c
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -78,10 +78,11 @@ typedef struct {
     GCancellable              *cancellable;
     NMDeviceAttachPortCallback callback;
     gpointer                   callback_user_data;
+    gboolean                   add;
 } AttachPortData;
 
 static void
-add_iface_cb(GError *error, gpointer user_data)
+add_del_iface_cb(GError *error, gpointer user_data)
 {
     AttachPortData       *data = user_data;
     NMDeviceOvsPort      *self;
@@ -93,15 +94,17 @@ add_iface_cb(GError *error, gpointer user_data)
     } else if (error && !nm_utils_error_is_cancelled_or_disposing(error)) {
         self = NM_DEVICE_OVS_PORT(data->device);
         _LOGW(LOGD_DEVICE,
-              "device %s could not be added to a ovs port: %s",
+              "device %s could not be %s a ovs port: %s",
               nm_device_get_iface(data->port),
+              data->add ? "added to" : "removed from",
               error->message);
         nm_device_state_changed(data->port,
                                 NM_DEVICE_STATE_FAILED,
                                 NM_DEVICE_STATE_REASON_OVSDB_FAILED);
     }
 
-    data->callback(data->device, error, data->callback_user_data);
+    if (data->callback)
+        data->callback(data->device, error, data->callback_user_data);
 
     g_object_unref(data->device);
     g_object_unref(data->port);
@@ -178,6 +181,7 @@ attach_port(NMDevice                  *device,
         .cancellable        = g_object_ref(cancellable),
         .callback           = callback,
         .callback_user_data = user_data,
+        .add                = TRUE,
     };
 
     nm_ovsdb_add_interface(nm_ovsdb_get(),
@@ -186,7 +190,7 @@ attach_port(NMDevice                  *device,
                            nm_device_get_applied_connection(port),
                            bridge_device,
                            port,
-                           add_iface_cb,
+                           add_del_iface_cb,
                            data);
 
     /* DPDK ports does not have a link after the devbind, so the MTU must be
@@ -205,29 +209,19 @@ attach_port(NMDevice                  *device,
     return NM_TERNARY_DEFAULT;
 }
 
-static void
-del_iface_cb(GError *error, gpointer user_data)
-{
-    NMDevice *slave = user_data;
-
-    if (error && !g_error_matches(error, NM_UTILS_ERROR, NM_UTILS_ERROR_CANCELLED_DISPOSING)) {
-        nm_log_warn(LOGD_DEVICE,
-                    "device %s could not be removed from a ovs port: %s",
-                    nm_device_get_iface(slave),
-                    error->message);
-        nm_device_state_changed(slave, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
-    }
-
-    g_object_unref(slave);
-}
-
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceOvsPort *self             = NM_DEVICE_OVS_PORT(device);
     bool             port_not_managed = !NM_IN_SET(nm_device_sys_iface_state_get(port),
                                        NM_DEVICE_SYS_IFACE_STATE_MANAGED,
                                        NM_DEVICE_SYS_IFACE_STATE_ASSUME);
+    NMTernary        ret              = TRUE;
 
     _LOGI(LOGD_DEVICE, "detaching ovs interface %s", nm_device_get_ip_iface(port));
 
@@ -236,10 +230,20 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
      * to make sure its OVSDB entry is gone.
      */
     if (configure || port_not_managed) {
-        nm_ovsdb_del_interface(nm_ovsdb_get(),
-                               nm_device_get_iface(port),
-                               del_iface_cb,
-                               g_object_ref(port));
+        AttachPortData *data;
+
+        data  = g_slice_new(AttachPortData);
+        *data = (AttachPortData){
+            .device             = g_object_ref(device),
+            .port               = g_object_ref(port),
+            .cancellable        = nm_g_object_ref(cancellable),
+            .callback           = callback,
+            .callback_user_data = user_data,
+            .add                = FALSE,
+        };
+
+        nm_ovsdb_del_interface(nm_ovsdb_get(), nm_device_get_iface(port), add_del_iface_cb, data);
+        ret = NM_TERNARY_DEFAULT;
     }
 
     if (configure) {
@@ -248,6 +252,8 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
         if (NM_IS_DEVICE_OVS_INTERFACE(port))
             nm_device_update_from_platform_link(port, NULL);
     }
+
+    return ret;
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
index 50023778..2ca1a0b5 100644
--- a/src/core/devices/ovs/nm-ovs-factory.c
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -240,9 +240,11 @@ ovsdb_interface_failed(NMOvsdb         *ovsdb,
         return;
 
     if (connection) {
-        nm_settings_connection_autoconnect_blocked_reason_set(
+        nm_manager_devcon_autoconnect_blocked_reason_set(
+            nm_device_get_manager(device),
+            device,
             connection,
-            NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
             TRUE);
     }
 
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
index 85b7953f..68366f73 100644
--- a/src/core/devices/ovs/nm-ovsdb.c
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -12,6 +12,7 @@
 
 #include "libnm-glib-aux/nm-jansson.h"
 #include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 #include "nm-core-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "devices/nm-device.h"
@@ -134,14 +135,18 @@ enum {
 static guint signals[LAST_SIGNAL] = {0};
 
 typedef struct {
-    NMPlatform        *platform;
-    GSocketConnection *conn;
-    GCancellable      *conn_cancellable;
-    char               buf[4096]; /* Input buffer */
-    size_t             bufp;      /* Last decoded byte in the input buffer. */
-    GString           *input;     /* JSON stream waiting for decoding. */
-    GString           *output;    /* JSON stream to be sent. */
-    guint64            call_id_counter;
+    NMPlatform   *platform;
+    int           conn_fd;
+    GSource      *conn_fd_in_source;
+    GSource      *conn_fd_out_source;
+    GCancellable *conn_cancellable;
+
+    NMStrBuf input_buf;
+    NMStrBuf output_buf;
+
+    GSource *input_timeout_source;
+
+    guint64 call_id_counter;
 
     CList calls_lst_head;
 
@@ -177,12 +182,13 @@ NM_DEFINE_SINGLETON_GETTER(NMOvsdb, nm_ovsdb_get, NM_TYPE_OVSDB);
 
 /*****************************************************************************/
 
-static void ovsdb_try_connect(NMOvsdb *self);
-static void ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing);
-static void ovsdb_read(NMOvsdb *self);
-static void ovsdb_write(NMOvsdb *self);
-static void ovsdb_next_command(NMOvsdb *self);
-static void cleanup_check_ready(NMOvsdb *self);
+static void     ovsdb_try_connect(NMOvsdb *self);
+static void     ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing);
+static void     ovsdb_read(NMOvsdb *self);
+static void     ovsdb_write_try(NMOvsdb *self);
+static gboolean ovsdb_write_cb(int fd, GIOCondition condition, gpointer user_data);
+static void     ovsdb_next_command(NMOvsdb *self);
+static void     cleanup_check_ready(NMOvsdb *self);
 
 /*****************************************************************************/
 
@@ -1446,10 +1452,10 @@ ovsdb_next_command(NMOvsdb *self)
 {
     NMOvsdbPrivate             *priv = NM_OVSDB_GET_PRIVATE(self);
     OvsdbMethodCall            *call;
-    char                       *cmd;
+    nm_auto_free char          *cmd = NULL;
     nm_auto_decref_json json_t *msg = NULL;
 
-    if (!priv->conn)
+    if (priv->conn_fd < 0)
         return;
 
     if (c_list_is_empty(&priv->calls_lst_head))
@@ -1586,10 +1592,9 @@ ovsdb_next_command(NMOvsdb *self)
 
     cmd = json_dumps(msg, 0);
     _LOGT_call(call, "send: call-id=%" G_GUINT64_FORMAT ", %s", call->call_id, cmd);
-    g_string_append(priv->output, cmd);
-    free(cmd);
+    nm_str_buf_append(&priv->output_buf, cmd);
 
-    ovsdb_write(self);
+    ovsdb_write_try(self);
 }
 
 /**
@@ -2188,20 +2193,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
 static void
 ovsdb_got_echo(NMOvsdb *self, json_int_t id, json_t *data)
 {
-    NMOvsdbPrivate             *priv = NM_OVSDB_GET_PRIVATE(self);
-    nm_auto_decref_json json_t *msg  = NULL;
-    char                       *reply;
-    gboolean                    output_was_empty;
-
-    output_was_empty = priv->output->len == 0;
+    NMOvsdbPrivate             *priv  = NM_OVSDB_GET_PRIVATE(self);
+    nm_auto_decref_json json_t *msg   = NULL;
+    nm_auto_free char          *reply = NULL;
 
     msg   = json_pack("{s:I, s:O}", "id", id, "result", data);
     reply = json_dumps(msg, 0);
-    g_string_append(priv->output, reply);
-    free(reply);
 
-    if (output_was_empty)
-        ovsdb_write(self);
+    _LOGT("send: echo: %s", reply);
+
+    nm_str_buf_append(&priv->output_buf, reply);
+
+    ovsdb_write_try(self);
 }
 
 /**
@@ -2274,13 +2277,13 @@ ovsdb_got_msg(NMOvsdb *self, json_t *msg)
 
         /* This is a response to a method call. */
         if (c_list_is_empty(&priv->calls_lst_head)) {
-            _LOGE("there are no queued calls expecting response %" G_GUINT64_FORMAT, (guint64) id);
+            _LOGW("there are no queued calls expecting response %" G_GUINT64_FORMAT, (guint64) id);
             ovsdb_disconnect(self, FALSE, FALSE);
             return;
         }
         call = c_list_first_entry(&priv->calls_lst_head, OvsdbMethodCall, calls_lst);
         if (call->call_id != id) {
-            _LOGE("expected a response to call %" G_GUINT64_FORMAT ", not %" G_GUINT64_FORMAT,
+            _LOGW("expected a response to call %" G_GUINT64_FORMAT ", not %" G_GUINT64_FORMAT,
                   call->call_id,
                   (guint64) id);
             ovsdb_disconnect(self, FALSE, FALSE);
@@ -2305,7 +2308,7 @@ ovsdb_got_msg(NMOvsdb *self, json_t *msg)
 
         /* Don't progress further commands in case the callback hit an error
          * and disconnected us. */
-        if (!priv->conn)
+        if (priv->conn_fd < 0)
             return;
 
         /* Now we're free to serialize and send the next command, if any. */
@@ -2320,138 +2323,197 @@ ovsdb_got_msg(NMOvsdb *self, json_t *msg)
 
 /*****************************************************************************/
 
+typedef struct {
+    gsize     bufp;
+    NMStrBuf *input;
+} JsonReadMsgData;
+
 /* Lower level marshalling and demarshalling of the JSON-RPC traffic on the
  * ovsdb socket. */
 
 static size_t
-_json_callback(void *buffer, size_t buflen, void *user_data)
+_json_read_msg_cb(void *buffer, size_t buflen, void *user_data)
 {
-    NMOvsdb        *self = NM_OVSDB(user_data);
-    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+    JsonReadMsgData *data = user_data;
+
+    nm_assert(buffer);
+    nm_assert(buflen > 0);
 
-    if (priv->bufp == priv->input->len) {
+    if (data->bufp == data->input->len) {
         /* No more bytes buffered for decoding. */
         return 0;
     }
 
     /* Pass one more byte to the JSON decoder. */
-    *(char *) buffer = priv->input->str[priv->bufp];
-    priv->bufp++;
-
-    return (size_t) 1;
+    *(char *) buffer = nm_str_buf_get_char(data->input, data->bufp);
+    data->bufp++;
+    return 1;
 }
 
-/**
- * ovsdb_read_cb:
- *
- * Read out the data available from the ovsdb socket and try to deserialize
- * the JSON. If we see a complete object, pass it upwards to ovsdb_got_msg().
- */
-static void
-ovsdb_read_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+static json_t *
+_json_read_msg(NMOvsdb *self, NMStrBuf *input)
 {
-    NMOvsdb        *self   = NM_OVSDB(user_data);
-    NMOvsdbPrivate *priv   = NM_OVSDB_GET_PRIVATE(self);
-    GInputStream   *stream = G_INPUT_STREAM(source_object);
-    GError         *error  = NULL;
-    gssize          size;
-    json_t         *msg;
-    json_error_t    json_error = {
+    gs_free char   *ss   = NULL;
+    JsonReadMsgData data = {
+        .bufp  = 0,
+        .input = input,
+    };
+    json_error_t json_error = {
         0,
     };
+    json_t *msg;
 
-    size = g_input_stream_read_finish(stream, res, &error);
-    if (size == -1) {
-        /* ovsdb-server was possibly restarted */
-        _LOGW("short read from ovsdb: %s", error->message);
-        priv->num_failures++;
-        g_clear_error(&error);
-        ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
-        return;
-    }
+    /* The callback always eats up only up to a single byte. This makes it
+     * possible for us to identify complete JSON objects in spite of us not
+     * knowing the length in advance. */
+    msg = json_load_callback(_json_read_msg_cb, &data, JSON_DISABLE_EOF_CHECK, &json_error);
+    if (!msg)
+        return NULL;
 
-    g_string_append_len(priv->input, priv->buf, size);
-    do {
-        priv->bufp = 0;
-        /* The callback always eats up only up to a single byte. This makes
-         * it possible for us to identify complete JSON objects in spite of
-         * us not knowing the length in advance. */
-        msg = json_load_callback(_json_callback, self, JSON_DISABLE_EOF_CHECK, &json_error);
-        if (msg) {
-            ovsdb_got_msg(self, msg);
-            g_string_erase(priv->input, 0, priv->bufp);
-        }
-        json_decref(msg);
-    } while (msg);
+    nm_assert(data.bufp > 0);
 
-    if (!priv->conn)
-        return;
+    _LOGT("json: parse %zu bytes: \"%s\"",
+          data.bufp,
+          (ss = g_strndup(nm_str_buf_get_str_at_unsafe(input, 0), data.bufp)));
 
-    if (size)
-        ovsdb_read(self);
+    nm_str_buf_erase(input, 0, data.bufp, FALSE);
+    return msg;
 }
 
-static void
-ovsdb_read(NMOvsdb *self)
+static gboolean
+_ovsdb_read_input_timeout_cb(gpointer user_data)
 {
+    NMOvsdb        *self = user_data;
     NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
 
-    g_input_stream_read_async(g_io_stream_get_input_stream(G_IO_STREAM(priv->conn)),
-                              priv->buf,
-                              sizeof(priv->buf),
-                              G_PRIORITY_DEFAULT,
-                              NULL,
-                              ovsdb_read_cb,
-                              self);
+    _LOGW("invalid/incomplete data in receive buffer. Reset");
+    priv->num_failures++;
+    ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
+    return G_SOURCE_CONTINUE;
 }
 
 static void
-ovsdb_write_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
+ovsdb_read(NMOvsdb *self)
 {
-    GOutputStream  *stream = G_OUTPUT_STREAM(source_object);
-    NMOvsdb        *self   = NM_OVSDB(user_data);
-    NMOvsdbPrivate *priv   = NM_OVSDB_GET_PRIVATE(self);
-    GError         *error  = NULL;
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
     gssize          size;
 
-    size = g_output_stream_write_finish(stream, res, &error);
-    if (size == -1) {
+again:
+    size = nm_utils_fd_read(priv->conn_fd, &priv->input_buf);
+
+    if (size <= 0) {
+        if (size == -EAGAIN) {
+            if (priv->input_buf.len == 0)
+                nm_clear_g_source_inst(&priv->input_timeout_source);
+            else if (!priv->input_timeout_source) {
+                /* We have data in the buffer but nothing further to read. Schedule a timer,
+                 * if we don't get the rest within timeout, it means that the buffer
+                 * content is broken (_json_read_msg() cannot extract any data) and
+                 * we disconnect. */
+                priv->input_timeout_source =
+                    nm_g_timeout_add_seconds_source(5, _ovsdb_read_input_timeout_cb, NULL);
+            }
+            return;
+        }
+
         /* ovsdb-server was possibly restarted */
-        _LOGW("short write to ovsdb: %s", error->message);
+        _LOGW("short read from ovsdb: %s", nm_strerror_native(-size));
         priv->num_failures++;
-        g_clear_error(&error);
         ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
         return;
     }
 
-    if (!priv->conn)
-        return;
+    nm_assert(priv->input_buf.len > 0);
+
+    while (TRUE) {
+        nm_auto_decref_json json_t *msg = NULL;
+
+        msg = _json_read_msg(self, &priv->input_buf);
+        if (!msg)
+            break;
 
-    g_string_erase(priv->output, 0, size);
+        nm_clear_g_source_inst(&priv->input_timeout_source);
+        ovsdb_got_msg(self, msg);
 
-    ovsdb_write(self);
+        if (priv->input_buf.len == 0)
+            break;
+    }
+
+    if (priv->input_buf.len > 0) {
+        if (priv->input_buf.len > 50 * 1024 * 1024) {
+            _LOGW("received too much data from ovsdb that is not valid JSON");
+            priv->num_failures++;
+            ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
+            return;
+        }
+        /* We have an incomplete message in the message buffer. Don't wait for another round
+         * of "poll", instead try to read it again. */
+        goto again;
+    }
+
+    nm_clear_g_source_inst(&priv->input_timeout_source);
+}
+
+static gboolean
+ovsdb_read_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    ovsdb_read(user_data);
+    return G_SOURCE_CONTINUE;
 }
 
 static void
 ovsdb_write(NMOvsdb *self)
 {
     NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
-    GOutputStream  *stream;
+    gssize          n;
 
-    if (!priv->output->len)
+again:
+    if (priv->output_buf.len == 0) {
+        nm_clear_g_source_inst(&priv->conn_fd_out_source);
         return;
+    }
+
+    n = write(priv->conn_fd,
+              nm_str_buf_get_str_at_unsafe(&priv->output_buf, 0),
+              priv->output_buf.len);
+
+    if (n < 0)
+        n = -NM_ERRNO_NATIVE(errno);
 
-    stream = g_io_stream_get_output_stream(G_IO_STREAM(priv->conn));
-    if (g_output_stream_has_pending(stream))
+    if (n == -EAGAIN) {
+        if (!priv->conn_fd_out_source) {
+            priv->conn_fd_out_source =
+                nm_g_unix_fd_add_source(priv->conn_fd, G_IO_OUT, ovsdb_write_cb, self);
+        }
         return;
+    }
+
+    if (n <= 0) {
+        /* ovsdb-server was possibly restarted */
+        _LOGW("short write to ovsdb: %s", nm_strerror_native(-n));
+        priv->num_failures++;
+        ovsdb_disconnect(self, priv->num_failures <= OVSDB_MAX_FAILURES, FALSE);
+        return;
+    }
 
-    g_output_stream_write_async(stream,
-                                priv->output->str,
-                                priv->output->len,
-                                G_PRIORITY_DEFAULT,
-                                NULL,
-                                ovsdb_write_cb,
-                                self);
+    nm_str_buf_erase(&priv->output_buf, 0, n, FALSE);
+    goto again;
+}
+
+static void
+ovsdb_write_try(NMOvsdb *self)
+{
+    NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
+
+    if (priv->conn_fd >= 0 && !priv->conn_fd_out_source)
+        ovsdb_write(self);
+}
+
+static gboolean
+ovsdb_write_cb(int fd, GIOCondition condition, gpointer user_data)
+{
+    ovsdb_write(user_data);
+    return G_SOURCE_CONTINUE;
 }
 
 /*****************************************************************************/
@@ -2474,7 +2536,7 @@ ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing)
 
     nm_assert(!retry || !is_disposing);
 
-    if (!priv->conn && !priv->conn_cancellable)
+    if (priv->conn_fd < 0 && !priv->conn_cancellable)
         return;
 
     _LOGD("disconnecting from ovsdb, retry %d", retry);
@@ -2498,10 +2560,12 @@ ovsdb_disconnect(NMOvsdb *self, gboolean retry, gboolean is_disposing)
             _call_complete(call, NULL, error);
     }
 
-    priv->bufp = 0;
-    g_string_truncate(priv->input, 0);
-    g_string_truncate(priv->output, 0);
-    g_clear_object(&priv->conn);
+    nm_str_buf_reset(&priv->input_buf);
+    nm_str_buf_reset(&priv->output_buf);
+    nm_clear_fd(&priv->conn_fd);
+    nm_clear_g_source_inst(&priv->conn_fd_in_source);
+    nm_clear_g_source_inst(&priv->conn_fd_out_source);
+    nm_clear_g_source_inst(&priv->input_timeout_source);
     nm_clear_g_free(&priv->db_uuid);
     nm_clear_g_cancellable(&priv->conn_cancellable);
 
@@ -2702,15 +2766,12 @@ _ovsdb_connect_complete_with_fd(NMOvsdb *self, int fd_take)
     gs_unref_object GSocket *socket = NULL;
     gs_free_error GError    *error  = NULL;
 
-    socket = g_socket_new_from_fd(nm_steal_fd(&fd_take), &error);
-    if (!socket) {
-        _LOGT("connect: failure to open socket for new FD: %s", error->message);
-        ovsdb_disconnect(self, FALSE, FALSE);
-        return;
-    }
+    nm_clear_g_cancellable(&priv->conn_cancellable);
+
+    nm_io_fcntl_setfl_update_nonblock(fd_take);
 
-    priv->conn = g_socket_connection_factory_create_connection(socket);
-    g_clear_object(&priv->conn_cancellable);
+    priv->conn_fd           = nm_steal_fd(&fd_take);
+    priv->conn_fd_in_source = nm_g_unix_fd_add_source(priv->conn_fd, G_IO_IN, ovsdb_read_cb, self);
 
     ovsdb_read(self);
     ovsdb_next_command(self);
@@ -2784,7 +2845,7 @@ ovsdb_try_connect(NMOvsdb *self)
 {
     NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
 
-    if (priv->conn || priv->conn_cancellable)
+    if (priv->conn_fd >= 0 || priv->conn_cancellable)
         return;
 
     _LOGT("connect: start connecting socket %s on idle", NM_OVSDB_SOCKET);
@@ -2964,11 +3025,15 @@ nm_ovsdb_init(NMOvsdb *self)
 {
     NMOvsdbPrivate *priv = NM_OVSDB_GET_PRIVATE(self);
 
+    priv->conn_fd = -1;
+
+    priv->input_buf  = NM_STR_BUF_INIT(0, FALSE);
+    priv->output_buf = NM_STR_BUF_INIT(0, FALSE);
+
     c_list_init(&priv->calls_lst_head);
 
     priv->platform = g_object_ref(NM_PLATFORM_GET);
-    priv->input    = g_string_new(NULL);
-    priv->output   = g_string_new(NULL);
+
     priv->bridges =
         g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, (GDestroyNotify) _free_bridge, NULL);
     priv->ports =
@@ -2989,14 +3054,8 @@ dispose(GObject *object)
 
     nm_assert(c_list_is_empty(&priv->calls_lst_head));
 
-    if (priv->input) {
-        g_string_free(priv->input, TRUE);
-        priv->input = NULL;
-    }
-    if (priv->output) {
-        g_string_free(priv->output, TRUE);
-        priv->output = NULL;
-    }
+    nm_str_buf_destroy(&priv->input_buf);
+    nm_str_buf_destroy(&priv->output_buf);
 
     g_clear_object(&priv->platform);
     nm_clear_pointer(&priv->bridges, g_hash_table_destroy);
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index 5c955986..4d748362 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -899,8 +899,13 @@ attach_port(NMDevice                  *device,
     return TRUE;
 }
 
-static void
-detach_port(NMDevice *device, NMDevice *port, gboolean configure)
+static NMTernary
+detach_port(NMDevice                  *device,
+            NMDevice                  *port,
+            gboolean                   configure,
+            GCancellable              *cancellable,
+            NMDeviceAttachPortCallback callback,
+            gpointer                   user_data)
 {
     NMDeviceTeam        *self       = NM_DEVICE_TEAM(device);
     NMDeviceTeamPrivate *priv       = NM_DEVICE_TEAM_GET_PRIVATE(self);
@@ -950,6 +955,8 @@ detach_port(NMDevice *device, NMDevice *port, gboolean configure)
         _update_port_config(self, port_iface, "{}");
         g_hash_table_remove(priv->port_configs, port_iface);
     }
+
+    return TRUE;
 }
 
 static gboolean
diff --git a/src/core/devices/wifi/nm-device-iwd-p2p.c b/src/core/devices/wifi/nm-device-iwd-p2p.c
index 40e38321..73fd4716 100644
--- a/src/core/devices/wifi/nm-device-iwd-p2p.c
+++ b/src/core/devices/wifi/nm-device-iwd-p2p.c
@@ -126,14 +126,17 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMSettingWifiP2P  *s_wifi_p2p;
     GBytes            *wfd_ies;
     NMSettingIPConfig *s_ip;
 
     if (!NM_DEVICE_CLASS(nm_device_iwd_p2p_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_wifi_p2p =
diff --git a/src/core/devices/wifi/nm-device-iwd.c b/src/core/devices/wifi/nm-device-iwd.c
index e03227cd..47407a1e 100644
--- a/src/core/devices/wifi/nm-device-iwd.c
+++ b/src/core/devices/wifi/nm-device-iwd.c
@@ -159,7 +159,7 @@ ap_add_remove(NMDeviceIwd *self,
     }
 
     if (priv->enabled && !priv->iwd_autoconnect)
-        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+        nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
 
     if (recheck_available_connections)
         nm_device_recheck_available_connections(NM_DEVICE(self));
@@ -208,7 +208,7 @@ remove_all_aps(NMDeviceIwd *self)
         ap_add_remove(self, FALSE, ap, FALSE);
 
     if (!priv->iwd_autoconnect)
-        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+        nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
 
     nm_device_recheck_available_connections(NM_DEVICE(self));
 }
@@ -401,7 +401,7 @@ get_ordered_networks_cb(GObject *source, GAsyncResult *res, gpointer user_data)
 
     if (changed) {
         if (!priv->iwd_autoconnect)
-            nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+            nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
 
         nm_device_recheck_available_connections(NM_DEVICE(self));
     }
@@ -723,7 +723,10 @@ is_ap_known_network(NMIwdManager *manager, NMWifiAP *ap)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceIwd         *self = NM_DEVICE_IWD(device);
     NMDeviceIwdPrivate  *priv = NM_DEVICE_IWD_GET_PRIVATE(self);
@@ -739,7 +742,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     gsize                ssid_len;
 
     if (!NM_DEVICE_CLASS(nm_device_iwd_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_wireless = nm_connection_get_setting_wireless(connection);
@@ -1682,7 +1685,7 @@ failed:
 
         if (!priv->nm_autoconnect) {
             priv->nm_autoconnect = true;
-            nm_device_emit_recheck_auto_activate(device);
+            nm_device_recheck_auto_activate_schedule(device);
         }
     }
     g_variant_unref(value);
@@ -2302,9 +2305,10 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
          * 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);
+            nm_manager_devcon_autoconnect_retries_set(nm_device_get_manager(device),
+                                                      device,
+                                                      nm_act_request_get_settings_connection(req),
+                                                      0);
         }
 
         /* With priv->iwd_autoconnect, if we're assuming a connection because
@@ -2908,7 +2912,7 @@ state_changed(NMDeviceIwd *self, const char *new_state)
     if (!priv->iwd_autoconnect && NM_IN_STRSET(new_state, "disconnected")) {
         priv->nm_autoconnect = TRUE;
         if (!can_connect)
-            nm_device_emit_recheck_auto_activate(device);
+            nm_device_recheck_auto_activate_schedule(device);
     }
 }
 
@@ -3104,12 +3108,12 @@ config_changed(NMConfig           *config,
     NMDeviceIwdPrivate *priv       = NM_DEVICE_IWD_GET_PRIVATE(self);
     gboolean            old_iwd_ac = priv->iwd_autoconnect;
 
-    priv->iwd_autoconnect =
-        nm_config_data_get_device_config_boolean(config_data,
-                                                 NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_IWD_AUTOCONNECT,
-                                                 NM_DEVICE(self),
-                                                 TRUE,
-                                                 TRUE);
+    priv->iwd_autoconnect = nm_config_data_get_device_config_boolean_by_device(
+        config_data,
+        NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_IWD_AUTOCONNECT,
+        NM_DEVICE(self),
+        TRUE,
+        TRUE);
 
     if (old_iwd_ac != priv->iwd_autoconnect && priv->dbus_station_proxy && !priv->current_ap) {
         gs_unref_variant GVariant *value = NULL;
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.c b/src/core/devices/wifi/nm-device-olpc-mesh.c
index 4705f75c..436c7847 100644
--- a/src/core/devices/wifi/nm-device-olpc-mesh.c
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.c
@@ -270,7 +270,7 @@ companion_state_changed_cb(NMDeviceWifi       *companion,
     NMDeviceState     self_state = nm_device_get_state(NM_DEVICE(self));
 
     if (old_state > NM_DEVICE_STATE_DISCONNECTED && state <= NM_DEVICE_STATE_DISCONNECTED) {
-        nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+        nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
     }
 
     if (self_state < NM_DEVICE_STATE_PREPARE || self_state > NM_DEVICE_STATE_ACTIVATED
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
index 424464c1..fa8cb8fa 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.c
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -233,10 +233,13 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     if (!NM_DEVICE_CLASS(nm_device_wifi_p2p_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     /* TODO: Allow limitting the interface using the HW-address? */
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 03625f8d..43772834 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -483,7 +483,7 @@ _scan_notify_is_scanning(NMDeviceWifi *self)
 
     if (!_scan_is_scanning_eval(priv)) {
         if (state <= NM_DEVICE_STATE_DISCONNECTED || state > NM_DEVICE_STATE_ACTIVATED)
-            nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+            nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
         nm_device_remove_pending_action(NM_DEVICE(self), NM_PENDING_ACTION_WIFI_SCAN, FALSE);
     }
 
@@ -843,7 +843,7 @@ ap_add_remove(NMDeviceWifi *self,
         nm_dbus_object_clear_and_unexport(&ap);
     }
 
-    nm_device_emit_recheck_auto_activate(NM_DEVICE(self));
+    nm_device_recheck_auto_activate_schedule(NM_DEVICE(self));
     if (recheck_available_connections)
         nm_device_recheck_available_connections(NM_DEVICE(self));
 }
@@ -981,7 +981,10 @@ deactivate_reset_hw_addr(NMDevice *device)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     NMDeviceWifi              *self = NM_DEVICE_WIFI(device);
     NMDeviceWifiPrivate       *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
@@ -995,7 +998,7 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     const char                *key_mgmt;
 
     if (!NM_DEVICE_CLASS(nm_device_wifi_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     s_wireless = nm_connection_get_setting_wireless(connection);
@@ -1395,7 +1398,7 @@ _hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset)
 
     priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
 
-    randomize = nm_config_data_get_device_config_boolean(
+    randomize = nm_config_data_get_device_config_boolean_by_device(
         NM_CONFIG_GET_DATA,
         NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_RAND_MAC_ADDRESS,
         device,
@@ -1428,7 +1431,7 @@ _hw_addr_set_scanning(NMDeviceWifi *self, gboolean do_reset)
          * a new one.*/
         priv->hw_addr_scan_expire = now + SCAN_RAND_MAC_ADDRESS_EXPIRE_SEC;
 
-        generate_mac_address_mask = nm_config_data_get_device_config(
+        generate_mac_address_mask = nm_config_data_get_device_config_by_device(
             NM_CONFIG_GET_DATA,
             NM_CONFIG_KEYFILE_KEY_DEVICE_WIFI_SCAN_GENERATE_MAC_ADDRESS_MASK,
             device,
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
index b83120f9..a1050c3f 100644
--- a/src/core/devices/wwan/nm-device-modem.c
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -383,12 +383,15 @@ get_type_description(NMDevice *device)
 }
 
 static gboolean
-check_connection_compatible(NMDevice *device, NMConnection *connection, GError **error)
+check_connection_compatible(NMDevice     *device,
+                            NMConnection *connection,
+                            gboolean      check_properties,
+                            GError      **error)
 {
     GError *local = NULL;
 
     if (!NM_DEVICE_CLASS(nm_device_modem_parent_class)
-             ->check_connection_compatible(device, connection, error))
+             ->check_connection_compatible(device, connection, check_properties, error))
         return FALSE;
 
     if (!nm_modem_check_connection_compatible(NM_DEVICE_MODEM_GET_PRIVATE(device)->modem,
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index f0907c46..a150040f 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -20,6 +20,8 @@
 
 #define NM_MODEM_BROADBAND_MODEM "modem"
 
+#define MM_SUPPORTS_INITIAL_EPS_BEARER_SETTINGS MM_CHECK_VERSION(1, 10, 0)
+
 #if !MM_CHECK_VERSION(1, 14, 0)
 #define MM_MODEM_CAPABILITY_5GNR ((MMModemCapability) (1 << 6))
 #endif
@@ -44,6 +46,7 @@ typedef enum {
     CONNECT_STEP_WAIT_FOR_SIM,
     CONNECT_STEP_UNLOCK,
     CONNECT_STEP_WAIT_FOR_READY,
+    CONNECT_STEP_INTIAL_EPS_BEARER,
     CONNECT_STEP_CONNECT,
     CONNECT_STEP_LAST,
 } ConnectStep;
@@ -560,6 +563,36 @@ out:
     return TRUE;
 }
 
+#if MM_SUPPORTS_INITIAL_EPS_BEARER_SETTINGS
+static void
+set_initial_eps_bearer_settings_ready(MMModem3gpp      *modem_3gpp_iface,
+                                      GAsyncResult     *res,
+                                      NMModemBroadband *self)
+{
+    gs_free_error GError *error = NULL;
+
+    if (!mm_modem_3gpp_set_initial_eps_bearer_settings_finish(modem_3gpp_iface, res, &error)) {
+        if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+            return;
+
+        if (!g_error_matches(error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED)) {
+            _LOGW("failed to set initial EPS bearer settings: %s", error->message);
+            nm_modem_emit_prepare_result(NM_MODEM(self),
+                                         FALSE,
+                                         NM_DEVICE_STATE_REASON_GSM_APN_FAILED);
+            connect_context_clear(self);
+            return;
+        }
+
+        _LOGD("failed to set initial EPS bearer settings due to lack of support: %s",
+              error->message);
+    }
+
+    self->_priv.ctx->step++;
+    connect_context_step(self);
+}
+#endif
+
 static void
 connect_context_step(NMModemBroadband *self)
 {
@@ -629,6 +662,56 @@ connect_context_step(NMModemBroadband *self)
         ctx->step++;
     }
         /* fall-through */
+
+    case CONNECT_STEP_INTIAL_EPS_BEARER:
+        if (MODEM_CAPS_3GPP(ctx->caps)) {
+            NMSettingGsm *s_gsm     = nm_connection_get_setting_gsm(ctx->connection);
+            const char   *apn       = nm_setting_gsm_get_initial_eps_apn(s_gsm);
+            gboolean      do_config = nm_setting_gsm_get_initial_eps_config(s_gsm);
+
+            /* assume do_config is true if an APN is set */
+            if (apn || do_config) {
+#if MM_SUPPORTS_INITIAL_EPS_BEARER_SETTINGS
+                gs_unref_object MMBearerProperties *config = NULL;
+                NMModemIPType ip_type = nm_modem_get_initial_eps_bearer_ip_type(ctx->ip_types);
+
+                config = mm_bearer_properties_new();
+                switch (ip_type) {
+                case NM_MODEM_IP_TYPE_IPV4:
+                    mm_bearer_properties_set_ip_type(config, MM_BEARER_IP_FAMILY_IPV4);
+                    break;
+                case NM_MODEM_IP_TYPE_IPV6:
+                    mm_bearer_properties_set_ip_type(config, MM_BEARER_IP_FAMILY_IPV6);
+                    break;
+                case NM_MODEM_IP_TYPE_IPV4V6:
+                    mm_bearer_properties_set_ip_type(config, MM_BEARER_IP_FAMILY_IPV4V6);
+                    break;
+                default:
+                    /* do nothing */
+                    break;
+                }
+                if (apn)
+                    mm_bearer_properties_set_apn(config, apn);
+
+                /*
+                 * Setting the initial EPS bearer settings is a no-op in
+                 * ModemManager if the desired configuration is already active.
+                 */
+                mm_modem_3gpp_set_initial_eps_bearer_settings(
+                    self->_priv.modem_3gpp_iface,
+                    config,
+                    ctx->cancellable,
+                    (GAsyncReadyCallback) set_initial_eps_bearer_settings_ready,
+                    self);
+                break;
+#else
+                _LOGD("cannot set initial EPS bearer settings due to old ModemManager version");
+#endif
+            }
+        }
+        ctx->step++;
+        /* fall-through */
+
     case CONNECT_STEP_CONNECT:
         if (!ctx->connect_properties)
             break;
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
index 0159d351..ea0fa7aa 100644
--- a/src/core/devices/wwan/nm-modem.c
+++ b/src/core/devices/wwan/nm-modem.c
@@ -558,6 +558,37 @@ nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError
     return NULL;
 }
 
+/**
+ * nm_modem_get_initial_eps_bearer_ip_type:
+ * @connection_ip_types: the #NMModemIPType as returned by
+ * nm_modem_get_connection_ip_type
+ *
+ * Given the connection IP types, this function returns which IP type to use when
+ * configuring the initial EPS bearer.
+ *
+ * Returns: the #NMModemIpType value to use for the initial EPS bearer
+ */
+NMModemIPType
+nm_modem_get_initial_eps_bearer_ip_type(const GArray *connection_ip_types)
+{
+    NMModemIPType ip_types = NM_MODEM_IP_TYPE_UNKNOWN;
+    guint         i;
+
+    nm_assert(connection_ip_types);
+
+    for (i = 0; i < connection_ip_types->len; i++)
+        ip_types |= nm_g_array_index(connection_ip_types, NMModemIPType, i);
+
+    nm_assert(ip_types != NM_MODEM_IP_TYPE_UNKNOWN);
+
+    if (ip_types & NM_MODEM_IP_TYPE_IPV4V6)
+        return NM_MODEM_IP_TYPE_IPV4V6;
+    if (ip_types & NM_MODEM_IP_TYPE_IPV4)
+        return NM_MODEM_IP_TYPE_IPV4;
+
+    return NM_MODEM_IP_TYPE_IPV6;
+}
+
 const char *
 nm_modem_get_device_id(NMModem *self)
 {
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
index ec001102..021d77b2 100644
--- a/src/core/devices/wwan/nm-modem.h
+++ b/src/core/devices/wwan/nm-modem.h
@@ -226,6 +226,8 @@ void nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason);
 
 GArray *nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error);
 
+NMModemIPType nm_modem_get_initial_eps_bearer_ip_type(const GArray *connection_ip_types);
+
 /* For subclasses */
 
 void nm_modem_emit_signal_new_config(NMModem                  *self,
diff --git a/src/core/dhcp/nm-dhcp-client-logging.h b/src/core/dhcp/nm-dhcp-client-logging.h
index 2b0d8d06..3a2b927a 100644
--- a/src/core/dhcp/nm-dhcp-client-logging.h
+++ b/src/core/dhcp/nm-dhcp-client-logging.h
@@ -41,16 +41,19 @@ _nm_dhcp_client_get_domain(NMDhcpClient *self)
         if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                 \
             NMDhcpClient     *_self    = (NMDhcpClient *) (self);                        \
             const char       *__ifname = _self ? nm_dhcp_client_get_iface(_self) : NULL; \
+            const char       *_type    = nm_dhcp_client_get_iface_type_for_log(_self);   \
             const NMLogDomain _domain  = _nm_dhcp_client_get_domain(_self);              \
                                                                                          \
             nm_log(_level,                                                               \
                    _domain,                                                              \
                    __ifname,                                                             \
                    NULL,                                                                 \
-                   "%s%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                    \
+                   "%s%s%s%s%s%s%s%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
                    _NMLOG_PREFIX_NAME,                                                   \
                    (_domain == LOGD_DHCP4 ? "4" : (_domain == LOGD_DHCP6 ? "6" : "")),   \
-                   NM_PRINT_FMT_QUOTED(__ifname, " (", __ifname, ")", "")                \
+                   (__ifname || _type) ? " " : "",                                       \
+                   NM_PRINT_FMT_QUOTED(__ifname, "(", __ifname, ")", ""),                \
+                   NM_PRINT_FMT_QUOTED(_type, "[", _type, "]", "")                       \
                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                               \
         }                                                                                \
     }                                                                                    \
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index b10ce410..6978bd3c 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -196,6 +196,14 @@ nm_dhcp_client_get_iface(NMDhcpClient *self)
     return priv->config.iface;
 }
 
+const char *
+nm_dhcp_client_get_iface_type_for_log(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->config.iface_type_log;
+}
+
 NMDedupMultiIndex *
 nm_dhcp_client_get_multi_idx(NMDhcpClient *self)
 {
@@ -1823,6 +1831,7 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
     nm_g_bytes_ref(config->client_id);
 
     config->iface           = g_strdup(config->iface);
+    config->iface_type_log  = g_strdup(config->iface_type_log);
     config->uuid            = g_strdup(config->uuid);
     config->anycast_address = g_strdup(config->anycast_address);
     config->hostname        = g_strdup(config->hostname);
@@ -1885,6 +1894,7 @@ config_clear(NMDhcpClientConfig *config)
     nm_clear_pointer(&config->client_id, g_bytes_unref);
 
     nm_clear_g_free((gpointer *) &config->iface);
+    nm_clear_g_free((gpointer *) &config->iface_type_log);
     nm_clear_g_free((gpointer *) &config->uuid);
     nm_clear_g_free((gpointer *) &config->anycast_address);
     nm_clear_g_free((gpointer *) &config->hostname);
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index f43770a0..903ea6ac 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -103,6 +103,10 @@ typedef struct {
 
     const char *iface;
 
+    /* Interface type for logging; only set for some devices whose names can be
+     * ambiguous. */
+    const char *iface_type_log;
+
     /* The hardware address */
     GBytes *hwaddr;
 
@@ -174,6 +178,10 @@ typedef struct {
             /* Number to prefixes (IA_PD) to request */
             guint needed_prefixes;
 
+            /* A hint to send to server for prefix delegation (IA_PD). */
+            struct in6_addr pd_hint_addr;
+            guint8          pd_hint_length;
+
             /* Use Information-request to get stateless configuration
              * parameters (don't request a IA_NA) */
             bool info_only : 1;
@@ -260,6 +268,7 @@ gboolean nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer
 
 int                nm_dhcp_client_get_addr_family(NMDhcpClient *self);
 const char        *nm_dhcp_client_get_iface(NMDhcpClient *self);
+const char        *nm_dhcp_client_get_iface_type_for_log(NMDhcpClient *self);
 NMDedupMultiIndex *nm_dhcp_client_get_multi_idx(NMDhcpClient *self);
 int                nm_dhcp_client_get_ifindex(NMDhcpClient *self);
 
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
index 35b2fb2e..4aab4b1e 100644
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ b/src/core/dhcp/nm-dhcp-dhclient.c
@@ -356,6 +356,7 @@ dhclient_start(NMDhcpClient *client,
     gs_free char                *preferred_leasefile_path = NULL;
     int                          addr_family;
     const NMDhcpClientConfig    *client_config;
+    char                         pd_length_str[16];
 
     g_return_val_if_fail(!priv->pid_file, FALSE);
     client_config = nm_dhcp_client_get_config(client);
@@ -463,6 +464,17 @@ dhclient_start(NMDhcpClient *client,
 
         if (mode_opt)
             g_ptr_array_add(argv, (gpointer) mode_opt);
+
+        if (prefixes > 0 && client_config->v6.pd_hint_length > 0) {
+            if (!IN6_IS_ADDR_UNSPECIFIED(&client_config->v6.pd_hint_addr)) {
+                _LOGW("dhclient only supports a length as prefix delegation hint, not a prefix");
+            }
+
+            nm_sprintf_buf(pd_length_str, "%u", client_config->v6.pd_hint_length);
+            g_ptr_array_add(argv, "--prefix-len-hint");
+            g_ptr_array_add(argv, pd_length_str);
+        }
+
         while (prefixes--)
             g_ptr_array_add(argv, (gpointer) "-P");
     }
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index 213d9496..ee95abb7 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -21,21 +21,22 @@
 #define _NMLOG_ENABLED(level) ((level) <= LOG_ERR)
 #endif
 
-#define _NMLOG(always_enabled, level, ...)                                                       \
-    G_STMT_START                                                                                 \
-    {                                                                                            \
-        if ((always_enabled) || _NMLOG_ENABLED(level)) {                                         \
-            GTimeVal _tv;                                                                        \
-                                                                                                 \
-            g_get_current_time(&_tv);                                                            \
-            g_print(                                                                             \
-                "nm-dhcp-helper[%ld] %-7s [%ld.%04ld] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) "\n", \
-                (long) getpid(),                                                                 \
-                nm_utils_syslog_to_str(level),                                                   \
-                _tv.tv_sec,                                                                      \
-                _tv.tv_usec / 100 _NM_UTILS_MACRO_REST(__VA_ARGS__));                            \
-        }                                                                                        \
-    }                                                                                            \
+#define _NMLOG(always_enabled, level, ...)                                     \
+    G_STMT_START                                                               \
+    {                                                                          \
+        if ((always_enabled) || _NMLOG_ENABLED(level)) {                       \
+            gint64 _tv;                                                        \
+                                                                               \
+            _tv = g_get_real_time();                                           \
+            g_print("nm-dhcp-helper[%ld] %-7s [%" G_GINT64_FORMAT              \
+                    ".%04d] " _NM_UTILS_MACRO_FIRST(__VA_ARGS__) "\n",         \
+                    (long) getpid(),                                           \
+                    nm_utils_syslog_to_str(level),                             \
+                    (_tv / NM_UTILS_USEC_PER_SEC),                             \
+                    ((int) ((_tv % NM_UTILS_USEC_PER_SEC) / (((gint64) 100)))) \
+                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                    \
+        }                                                                      \
+    }                                                                          \
     G_STMT_END
 
 #define _LOGD(...) _NMLOG(TRUE, LOG_INFO, __VA_ARGS__)
diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c
index 33a9f4ed..7c47c82e 100644
--- a/src/core/dhcp/nm-dhcp-options.c
+++ b/src/core/dhcp/nm-dhcp-options.c
@@ -199,6 +199,7 @@ const NMDhcpOption _nm_dhcp_option_dhcp6_options[] = {
 
     REQ(NM_DHCP_OPTION_DHCP6_DNS_SERVERS, "dhcp6_name_servers", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, "dhcp6_domain_search", TRUE),
+    REQ(NM_DHCP_OPTION_DHCP6_IA_PD, "ip6_prefix", FALSE),
     REQ(NM_DHCP_OPTION_DHCP6_SNTP_SERVERS, "dhcp6_sntp_servers", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_FQDN, "fqdn_fqdn", FALSE),
     REQ(NM_DHCP_OPTION_DHCP6_NTP_SERVER, "dhcp6_ntp_servers", TRUE),
@@ -237,6 +238,7 @@ static const NMDhcpOption *const _sorted_options_6[G_N_ELEMENTS(_nm_dhcp_option_
     A(14),
     A(15),
     A(16),
+    A(17),
 #undef A
 };
 
diff --git a/src/core/dhcp/nm-dhcp-options.h b/src/core/dhcp/nm-dhcp-options.h
index 050080d9..1c61c74d 100644
--- a/src/core/dhcp/nm-dhcp-options.h
+++ b/src/core/dhcp/nm-dhcp-options.h
@@ -161,6 +161,7 @@ typedef enum {
     NM_DHCP_OPTION_DHCP6_SERVER_ID    = 2,
     NM_DHCP_OPTION_DHCP6_DNS_SERVERS  = 23,
     NM_DHCP_OPTION_DHCP6_DOMAIN_LIST  = 24,
+    NM_DHCP_OPTION_DHCP6_IA_PD        = 25, /* RFC 8415 */
     NM_DHCP_OPTION_DHCP6_SNTP_SERVERS = 31,
     NM_DHCP_OPTION_DHCP6_FQDN         = 39,
     NM_DHCP_OPTION_DHCP6_NTP_SERVER   = 56, /* RFC 5908 */
@@ -188,7 +189,7 @@ typedef struct {
 } NMDhcpOption;
 
 extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[143];
-extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[17];
+extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[18];
 
 static inline const char *
 nm_dhcp_option_get_name(const NMDhcpOption *option)
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index 6f9312da..0fc5f928 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -155,6 +155,26 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
                                   str->str);
     }
 
+    {
+        struct in6_addr prefix;
+        uint8_t         prefix_len;
+
+        nm_gstring_prepare(&str);
+        sd_dhcp6_lease_reset_pd_prefix_iter(lease);
+        while (!sd_dhcp6_lease_get_pd(lease, &prefix, &prefix_len, NULL, NULL)) {
+            nm_gstring_add_space_delimiter(str);
+            nm_inet6_ntop(&prefix, addr_str);
+            g_string_append_printf(str, "%s/%u", addr_str, prefix_len);
+        }
+        if (str->len > 0) {
+            nm_dhcp_option_add_option(options,
+                                      TRUE,
+                                      AF_INET6,
+                                      NM_DHCP_OPTION_DHCP6_IA_PD,
+                                      str->str);
+        }
+    }
+
     num = sd_dhcp6_lease_get_domains(lease, &domains);
     if (num > 0) {
         nm_gstring_prepare(&str);
@@ -366,6 +386,15 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
             _LOGW("dhcp-client6: only one prefix request is supported");
         }
         prefix_delegation = TRUE;
+        if (client_config->v6.pd_hint_length > 0) {
+            r = sd_dhcp6_client_set_prefix_delegation_hint(sd_client,
+                                                           client_config->v6.pd_hint_length,
+                                                           &client_config->v6.pd_hint_addr);
+            if (r < 0) {
+                nm_utils_error_set_errno(error, r, "failed to set prefix delegation hint: %s");
+                return FALSE;
+            }
+        }
     }
     r = sd_dhcp6_client_set_prefix_delegation(sd_client, prefix_delegation);
     if (r < 0) {
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c
index 05aeff49..53e40f59 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -527,7 +527,6 @@ _gl_pid_spawn_next_step(void)
     argv[argv_idx++] = "--cache-size=400";
     argv[argv_idx++] = "--clear-on-reload";     /* clear cache when dns server changes */
     argv[argv_idx++] = "--conf-file=/dev/null"; /* avoid loading /etc/dnsmasq.conf */
-    argv[argv_idx++] = "--proxy-dnssec";        /* Allow DNSSEC to pass through */
     argv[argv_idx++] = "--enable-dbus=" DNSMASQ_DBUS_SERVICE;
 
     /* dnsmasq exits if the conf dir is not present */
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 6ee2e816..53564693 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -125,6 +125,9 @@ typedef struct {
 
     NMConfig *config;
 
+    NMDnsConfigIPData *best_ip_config_4;
+    NMDnsConfigIPData *best_ip_config_6;
+
     struct {
         guint64 ts;
         guint   num_restarts;
@@ -173,14 +176,46 @@ NM_DEFINE_SINGLETON_GETTER(NMDnsManager, nm_dns_manager_get, NM_TYPE_DNS_MANAGER
 /*****************************************************************************/
 
 static gboolean
-domain_is_valid(const char *domain, gboolean check_public_suffix)
+domain_is_valid(const char *domain,
+                gboolean    reject_public_suffix,
+                gboolean    assume_any_tld_is_public)
 {
     if (*domain == '\0')
         return FALSE;
-#if WITH_LIBPSL
-    if (check_public_suffix && psl_is_public_suffix(psl_builtin(), domain))
-        return FALSE;
+
+    if (reject_public_suffix) {
+        int is_pub;
+
+#if !WITH_LIBPSL
+        /* Without libpsl, we cannot detect that the domain is a public suffix, we assume
+         * the domain is not and valid. */
+        is_pub = FALSE;
+#elif defined(PSL_TYPE_NO_STAR_RULE)
+        /*
+         * If we use PSL_TYPE_ANY, any TLD (top-level domain, i.e., domain
+         * with no dots) is considered *public* by the PSL library even if
+         * it is *not* on the official suffix list. This is the implicit
+         * behavior of the older API function psl_is_public_suffix().
+         * To inhibit that and only deem TLDs explicitly listed in the PSL
+         * as public, we need to turn off the "prevailing star rule" with
+         * PSL_TYPE_NO_STAR_RULE.
+         * For documentation on psl_is_public_suffix2(), see:
+         * https://rockdaboot.github.io/libpsl/libpsl-Public-Suffix-List-functions.html#psl-is-public-suffix2
+         * For more on the public suffix format, including wildcards:
+         * https://github.com/publicsuffix/list/wiki/Format#format
+         */
+        is_pub =
+            psl_is_public_suffix2(psl_builtin(),
+                                  domain,
+                                  assume_any_tld_is_public ? PSL_TYPE_ANY : PSL_TYPE_NO_STAR_RULE);
+#else
+        is_pub = psl_is_public_suffix(psl_builtin(), domain);
 #endif
+
+        if (is_pub)
+            return FALSE;
+    }
+
     return TRUE;
 }
 
@@ -533,7 +568,7 @@ add_dns_domains(GPtrArray            *array,
         str = searches[i];
         if (!include_routing && domain_is_routing(str))
             continue;
-        if (!domain_is_valid(nm_utils_parse_dns_domain(str, NULL), FALSE))
+        if (!domain_is_valid(nm_utils_parse_dns_domain(str, NULL), FALSE, TRUE))
             continue;
         add_string_item(array, str, dup);
     }
@@ -542,7 +577,7 @@ add_dns_domains(GPtrArray            *array,
             str = domains[i];
             if (!include_routing && domain_is_routing(str))
                 continue;
-            if (!domain_is_valid(nm_utils_parse_dns_domain(str, NULL), FALSE))
+            if (!domain_is_valid(nm_utils_parse_dns_domain(str, NULL), FALSE, TRUE))
                 continue;
             add_string_item(array, str, dup);
         }
@@ -647,7 +682,7 @@ run_netconfig(NMDnsManager *self, GError **error, int *stdin_fd)
     if (!g_spawn_async_with_pipes(NULL,
                                   argv,
                                   NULL,
-                                  G_SPAWN_DO_NOT_REAP_CHILD,
+                                  G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD,
                                   NULL,
                                   NULL,
                                   &pid,
@@ -1236,7 +1271,7 @@ merge_global_dns_config(NMResolvConfData *rc, NMGlobalDnsConfig *global_conf)
         for (i = 0; searches[i]; i++) {
             if (domain_is_routing(searches[i]))
                 continue;
-            if (!domain_is_valid(searches[i], FALSE))
+            if (!domain_is_valid(searches[i], FALSE, TRUE))
                 continue;
             add_string_item(rc->searches, searches[i], TRUE);
         }
@@ -1946,6 +1981,7 @@ nm_dns_manager_set_ip_config(NMDnsManager         *self,
     NMDnsConfigIPData   *ip_data = NULL;
     int                  dns_priority;
     gboolean             any_removed = FALSE;
+    NMDnsConfigIPData  **p_best;
 
     g_return_val_if_fail(NM_IS_DNS_MANAGER(self), FALSE);
     g_return_val_if_fail(!l3cd || NM_IS_L3_CONFIG_DATA(l3cd), FALSE);
@@ -2013,6 +2049,12 @@ nm_dns_manager_set_ip_config(NMDnsManager         *self,
             }
 
             any_removed = TRUE;
+
+            if (priv->best_ip_config_4 == ip_data_iter)
+                priv->best_ip_config_4 = NULL;
+            if (priv->best_ip_config_6 == ip_data_iter)
+                priv->best_ip_config_6 = NULL;
+
             _dns_config_ip_data_free(ip_data_iter);
         }
     }
@@ -2063,6 +2105,19 @@ nm_dns_manager_set_ip_config(NMDnsManager         *self,
         changed                 = TRUE;
     }
 
+    p_best = NM_IS_IPv4(addr_family) ? &priv->best_ip_config_4 : &priv->best_ip_config_6;
+    if (ip_config_type == NM_DNS_IP_CONFIG_TYPE_BEST_DEVICE) {
+        /* Only one best-device per IP version is allowed */
+        if (*p_best != ip_data) {
+            if (*p_best)
+                (*p_best)->ip_config_type = NM_DNS_IP_CONFIG_TYPE_DEFAULT;
+            *p_best = ip_data;
+        }
+    } else {
+        if (*p_best == ip_data)
+            *p_best = NULL;
+    }
+
     if (changed)
         priv->ip_data_lst_need_sort = TRUE;
 
@@ -2100,7 +2155,8 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
 
     /* Certain hostnames we don't want to include in resolv.conf 'searches' */
     if (hostname && nm_utils_is_specific_hostname(hostname)
-        && !g_str_has_suffix(hostname, ".in-addr.arpa") && !nm_inet_is_valid(AF_UNSPEC, hostname)) {
+        && !NM_STR_HAS_SUFFIX(hostname, ".in-addr.arpa")
+        && !nm_inet_is_valid(AF_UNSPEC, hostname)) {
         domain = strchr(hostname, '.');
         if (domain) {
             domain++;
@@ -2111,11 +2167,16 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
              * specified, this makes a good default.) However, if the
              * hostname is the top level of a domain (eg, "example.com"),
              * then use the hostname itself as the search (since the user
-             * is unlikely to want "com" as a search domain).a
+             * is unlikely to want "com" as a search domain).
+             *
+             * Because that logic only applies to public domains, the
+             * "assume_any_tld_is_public" parameter is FALSE. For
+             * example, it is likely that the user *does* want "local"
+             * or "localdomain" as a search domain.
              */
-            if (domain_is_valid(domain, TRUE)) {
+            if (domain_is_valid(domain, TRUE, FALSE)) {
                 /* pass */
-            } else if (domain_is_valid(hostname, TRUE)) {
+            } else if (domain_is_valid(hostname, TRUE, FALSE)) {
                 domain = hostname;
             }
 
@@ -2127,6 +2188,8 @@ nm_dns_manager_set_hostname(NMDnsManager *self, const char *hostname, gboolean s
     if (!nm_strdup_reset(&priv->hostdomain, domain))
         return;
 
+    _LOGT("set host domain to %s%s%s", NM_PRINT_FMT_QUOTE_STRING(priv->hostdomain));
+
     if (skip_update)
         return;
 
@@ -2779,6 +2842,9 @@ dispose(GObject *object)
 
     nm_clear_g_source_inst(&priv->update_pending_unblock);
 
+    priv->best_ip_config_4 = NULL;
+    priv->best_ip_config_6 = NULL;
+
     c_list_for_each_entry_safe (ip_data, ip_data_safe, &priv->ip_data_lst_head, ip_data_lst)
         _dns_config_ip_data_free(ip_data);
 
diff --git a/src/core/main.c b/src/core/main.c
index 2eb230d9..4c7de6cd 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -266,13 +266,14 @@ _dbus_manager_init(NMConfig *config)
 
     c_a_q_type = nm_config_get_configure_and_quit(config);
 
-    if (c_a_q_type == NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED)
-        return nm_dbus_manager_acquire_bus(busmgr, TRUE);
+    if (c_a_q_type == NM_CONFIG_CONFIGURE_AND_QUIT_INITRD) {
+        /* in initrd we don't have D-Bus at all. Don't even try to get the G_BUS_TYPE_SYSTEM
+         * connection. And of course don't claim the D-Bus name. */
+        return TRUE;
+    }
 
-    nm_assert(c_a_q_type == NM_CONFIG_CONFIGURE_AND_QUIT_INITRD);
-    /* in initrd we don't have D-Bus at all. Don't even try to get the G_BUS_TYPE_SYSTEM
-     * connection. And of course don't claim the D-Bus name. */
-    return TRUE;
+    nm_assert(c_a_q_type == NM_CONFIG_CONFIGURE_AND_QUIT_DISABLED);
+    return nm_dbus_manager_setup(busmgr);
 }
 
 /*
@@ -507,6 +508,9 @@ main(int argc, char *argv[])
     nm_log_dbg(LOGD_CORE, "setting up local loopback");
     nm_platform_link_change_flags(NM_PLATFORM_GET, 1, IFF_UP, TRUE);
 
+    if (!nm_dbus_manager_request_name_sync(nm_dbus_manager_get()))
+        goto done;
+
     success = TRUE;
 
     if (configure_and_quit == FALSE) {
diff --git a/src/core/nm-act-request.c b/src/core/nm-act-request.c
index 55d1829e..dce18ba4 100644
--- a/src/core/nm-act-request.c
+++ b/src/core/nm-act-request.c
@@ -416,8 +416,8 @@ nm_act_request_init(NMActRequest *req)
 /**
  * nm_act_request_new:
  *
- * @settings_connection: (allow-none): the connection to activate @device with
- * @applied_connection: (allow-none): the applied connection
+ * @settings_connection: (nullable): the connection to activate @device with
+ * @applied_connection: (nullable): the applied connection
  * @specific_object: the object path of the specific object (ie, Wi-Fi access point,
  *    etc) that will be used to activate @connection and @device
  * @subject: the #NMAuthSubject representing the requestor of the activation
diff --git a/src/core/nm-active-connection.c b/src/core/nm-active-connection.c
index 6f62a601..36a11f71 100644
--- a/src/core/nm-active-connection.c
+++ b/src/core/nm-active-connection.c
@@ -1153,7 +1153,7 @@ auth_done(NMAuthManager       *auth_mgr,
 /**
  * nm_active_connection_authorize:
  * @self: the #NMActiveConnection
- * @initial_connection: (allow-none): for add-and-activate, there
+ * @initial_connection: (nullable): for add-and-activate, there
  *   is no @settings_connection available when creating the active connection.
  *   Instead pass an alternative connection.
  * @result_func: function to be called on success or error
diff --git a/src/core/nm-auth-utils.c b/src/core/nm-auth-utils.c
index cef926fd..7739f443 100644
--- a/src/core/nm-auth-utils.c
+++ b/src/core/nm-auth-utils.c
@@ -251,7 +251,7 @@ nm_auth_chain_steal_data(NMAuthChain *self, const char *tag)
  * @tag: the tag for referencing the attached data.
  * @data: the data to attach. If %NULL, this call has no effect
  *   and nothing is attached.
- * @data_destroy: (allow-none): the destroy function for the data pointer.
+ * @data_destroy: (nullable): the destroy function for the data pointer.
  *
  * @tag string is not cloned and must outlive @self. That is why
  * the function is "unsafe". Use nm_auth_chain_set_data() with a C literal
diff --git a/src/core/nm-bond-manager.c b/src/core/nm-bond-manager.c
index 2d15b0b5..9985fccf 100644
--- a/src/core/nm-bond-manager.c
+++ b/src/core/nm-bond-manager.c
@@ -438,6 +438,7 @@ _nft_call(NMBondManager     *self,
 {
     gs_unref_bytes GBytes     *stdin_buf             = NULL;
     gs_free const char *const *previous_members_strv = NULL;
+    gboolean                   with_counters;
 
     if (up) {
         gs_unref_ptrarray GPtrArray *arr = NULL;
@@ -480,11 +481,16 @@ _nft_call(NMBondManager     *self,
         }
     }
 
+    /* counters in the nft rules are convenient for debugging, but have a performance overhead.
+     * Enable counters based on whether NM logging is enabled. */
+    with_counters = _NMLOG_ENABLED(LOGL_TRACE);
+
     stdin_buf = nm_firewall_nft_stdio_mlag(up,
                                            bond_ifname,
                                            bond_ifnames_down,
                                            active_members,
-                                           previous_members_strv);
+                                           previous_members_strv,
+                                           with_counters);
 
     nm_clear_g_cancellable(&self->cancellable);
     self->cancellable = g_cancellable_new();
diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c
index cd0e17fa..5c4d4e53 100644
--- a/src/core/nm-checkpoint.c
+++ b/src/core/nm-checkpoint.c
@@ -259,17 +259,19 @@ restore_and_activate_connection(NMCheckpoint *self, DeviceCheckpoint *dev_checkp
             g_clear_error(&local_error);
             return FALSE;
         }
-
-        /* If the device is software, a brand new NMDevice may have been created */
-        if (dev_checkpoint->is_software && !dev_checkpoint->device) {
-            dev_checkpoint->device = nm_manager_get_device(priv->manager,
-                                                           dev_checkpoint->original_dev_name,
-                                                           dev_checkpoint->dev_type);
-            nm_g_object_ref(dev_checkpoint->device);
-        }
         need_activation = TRUE;
     }
 
+    /* If the device is software, a brand new NMDevice may have been created
+     * after adding the new connection; or the old device might have been
+     * deleted and we need to fetch it again. */
+    if (dev_checkpoint->is_software && !dev_checkpoint->device) {
+        dev_checkpoint->device = nm_manager_get_device(priv->manager,
+                                                       dev_checkpoint->original_dev_name,
+                                                       dev_checkpoint->dev_type);
+        nm_g_object_ref(dev_checkpoint->device);
+    }
+
     if (!dev_checkpoint->device) {
         _LOGD("rollback: device cannot be restored");
         return FALSE;
@@ -368,7 +370,7 @@ nm_checkpoint_rollback(NMCheckpoint *self)
                 _LOGD("rollback: device was not realized, unmanage it");
                 nm_device_set_unmanaged_by_flags_queue(device,
                                                        NM_UNMANAGED_USER_EXPLICIT,
-                                                       TRUE,
+                                                       NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                        NM_DEVICE_STATE_REASON_NOW_UNMANAGED);
                 goto next_dev;
             }
@@ -402,7 +404,7 @@ nm_checkpoint_rollback(NMCheckpoint *self)
                 _LOGD("rollback: explicitly unmanage device");
                 nm_device_set_unmanaged_by_flags_queue(device,
                                                        NM_UNMANAGED_USER_EXPLICIT,
-                                                       TRUE,
+                                                       NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                        NM_DEVICE_STATE_REASON_NOW_UNMANAGED);
             }
             goto next_dev;
@@ -458,8 +460,25 @@ next_dev:
         NMDeviceState state;
 
         nm_manager_for_each_device (priv->manager, device, tmp_lst) {
+            gboolean found = FALSE;
+
             if (g_hash_table_contains(priv->devices, device))
                 continue;
+
+            /* Also ignore devices that were in the checkpoint initially and
+             * were moved to 'removed_devices' because they got removed from
+             * the system. */
+            for (i = 0; i < priv->removed_devices->len; i++) {
+                dev_checkpoint = priv->removed_devices->pdata[i];
+                if (dev_checkpoint->dev_type == nm_device_get_device_type(device)
+                    && nm_streq0(dev_checkpoint->original_dev_name, nm_device_get_iface(device))) {
+                    found = TRUE;
+                    break;
+                }
+            }
+            if (found)
+                continue;
+
             state = nm_device_get_state(device);
             if (state > NM_DEVICE_STATE_DISCONNECTED && state < NM_DEVICE_STATE_DEACTIVATING) {
                 _LOGD("rollback: disconnecting new device %s", nm_device_get_iface(device));
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index ff44bc46..ed6d8381 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -130,6 +130,12 @@ G_DEFINE_TYPE(NMConfigData, nm_config_data, G_TYPE_OBJECT)
 static const char *
 _match_section_info_get_str(const MatchSectionInfo *m, GKeyFile *keyfile, const char *property);
 
+static const char *_config_data_get_device_config(const NMConfigData          *self,
+                                                  const char                  *property,
+                                                  const NMMatchSpecDeviceData *match_data,
+                                                  NMDevice                    *device,
+                                                  gboolean                    *has_match);
+
 /*****************************************************************************/
 
 const char *
@@ -366,7 +372,55 @@ nm_config_data_get_iwd_config_path(const NMConfigData *self)
 }
 
 gboolean
-nm_config_data_get_ignore_carrier(const NMConfigData *self, NMDevice *device)
+nm_config_data_get_ignore_carrier_for_port(const NMConfigData *self,
+                                           const char         *master,
+                                           const char         *slave_type)
+{
+    const char           *value;
+    gboolean              has_match;
+    int                   m;
+    NMMatchSpecDeviceData match_data;
+
+    g_return_val_if_fail(NM_IS_CONFIG_DATA(self), FALSE);
+
+    if (!master || !slave_type)
+        goto out_default;
+
+    if (!nm_utils_ifname_valid_kernel(master, NULL))
+        goto out_default;
+
+    match_data = (NMMatchSpecDeviceData){
+        .interface_name = master,
+        .device_type    = slave_type,
+    };
+
+    value = _config_data_get_device_config(self,
+                                           NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
+                                           &match_data,
+                                           NULL,
+                                           &has_match);
+    if (has_match)
+        m = nm_config_parse_boolean(value, -1);
+    else {
+        NMMatchSpecMatchType x;
+
+        x = nm_match_spec_device(NM_CONFIG_DATA_GET_PRIVATE(self)->ignore_carrier, &match_data);
+        m = nm_match_spec_match_type_to_bool(x, -1);
+    }
+
+    if (NM_IN_SET(m, TRUE, FALSE))
+        return m;
+
+out_default:
+    /* if ignore-carrier is not explicitly or detected for the master, then we assume it's
+     * enabled. This is in line with nm_config_data_get_ignore_carrier_by_device(), where
+     * ignore-carrier is enabled based on nm_device_ignore_carrier_by_default().
+     */
+    return TRUE;
+}
+
+gboolean
+nm_config_data_get_ignore_carrier_by_device(const NMConfigData *self, NMDevice *device)
 {
     const char *value;
     gboolean    has_match;
@@ -375,10 +429,10 @@ nm_config_data_get_ignore_carrier(const NMConfigData *self, NMDevice *device)
     g_return_val_if_fail(NM_IS_CONFIG_DATA(self), FALSE);
     g_return_val_if_fail(NM_IS_DEVICE(device), FALSE);
 
-    value = nm_config_data_get_device_config(self,
-                                             NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
-                                             device,
-                                             &has_match);
+    value = nm_config_data_get_device_config_by_device(self,
+                                                       NM_CONFIG_KEYFILE_KEY_DEVICE_IGNORE_CARRIER,
+                                                       device,
+                                                       &has_match);
     if (has_match)
         m = nm_config_parse_boolean(value, -1);
     else
@@ -706,6 +760,7 @@ static const struct {
 } default_values[] = {
     {NM_CONFIG_KEYFILE_GROUP_MAIN, "plugins", NM_CONFIG_DEFAULT_MAIN_PLUGINS},
     {NM_CONFIG_KEYFILE_GROUP_MAIN, "rc-manager", NM_CONFIG_DEFAULT_MAIN_RC_MANAGER},
+    {NM_CONFIG_KEYFILE_GROUP_MAIN, "migrate-ifcfg-rh", NM_CONFIG_DEFAULT_MAIN_MIGRATE_IFCFG_RH},
     {NM_CONFIG_KEYFILE_GROUP_MAIN,
      NM_CONFIG_KEYFILE_KEY_MAIN_AUTH_POLKIT,
      NM_CONFIG_DEFAULT_MAIN_AUTH_POLKIT},
@@ -1488,21 +1543,23 @@ global_dns_equal(NMGlobalDnsConfig *old, NMGlobalDnsConfig *new)
 /*****************************************************************************/
 
 static const MatchSectionInfo *
-_match_section_infos_lookup(const MatchSectionInfo *match_section_infos,
-                            GKeyFile               *keyfile,
-                            const char             *property,
-                            NMDevice               *device,
-                            const NMPlatformLink   *pllink,
-                            const char             *match_device_type,
-                            const char            **out_value)
+_match_section_infos_lookup(const MatchSectionInfo      *match_section_infos,
+                            GKeyFile                    *keyfile,
+                            const char                  *property,
+                            const NMMatchSpecDeviceData *match_data,
+                            NMDevice                    *device,
+                            const char                 **out_value)
 {
-    const char *match_dhcp_plugin;
+    NMMatchSpecDeviceData match_data_local;
+
+    /* Caller must either provide a "match_data" or a "device" (actually,
+     * neither is also fine, albeit unusual). */
+    nm_assert(!match_data || !device);
+    nm_assert(!device || NM_IS_DEVICE(device));
 
     if (!match_section_infos)
         goto out;
 
-    match_dhcp_plugin = nm_dhcp_manager_get_config(nm_dhcp_manager_get());
-
     for (; match_section_infos->group_name; match_section_infos++) {
         const char *value;
         gboolean    match;
@@ -1519,16 +1576,17 @@ _match_section_infos_lookup(const MatchSectionInfo *match_section_infos,
             continue;
 
         if (match_section_infos->match_device.has) {
-            if (device)
-                match = nm_device_spec_match_list(device, match_section_infos->match_device.spec);
-            else if (pllink)
-                match = nm_match_spec_device_by_pllink(pllink,
-                                                       match_device_type,
-                                                       match_dhcp_plugin,
-                                                       match_section_infos->match_device.spec,
-                                                       FALSE);
-            else
-                match = FALSE;
+            NMMatchSpecMatchType m;
+
+            if (G_UNLIKELY(!match_data)) {
+                /* In most cases, we don't actually have any matches. So we "optimize"
+                 * here by allowing the user to specify a NMDEvice directly, and only
+                 * initialize the match-data when needed. */
+                match_data = nm_match_spec_device_data_init_from_device(&match_data_local, device);
+            }
+
+            m     = nm_match_spec_device(match_section_infos->match_device.spec, match_data);
+            match = nm_match_spec_match_type_to_bool(m, FALSE);
         } else
             match = TRUE;
 
@@ -1543,11 +1601,12 @@ out:
     return NULL;
 }
 
-const char *
-nm_config_data_get_device_config(const NMConfigData *self,
-                                 const char         *property,
-                                 NMDevice           *device,
-                                 gboolean           *has_match)
+static const char *
+_config_data_get_device_config(const NMConfigData          *self,
+                               const char                  *property,
+                               const NMMatchSpecDeviceData *match_data,
+                               NMDevice                    *device,
+                               gboolean                    *has_match)
 {
     const NMConfigDataPrivate *priv;
     const MatchSectionInfo    *connection_info;
@@ -1558,20 +1617,40 @@ nm_config_data_get_device_config(const NMConfigData *self,
     g_return_val_if_fail(self, NULL);
     g_return_val_if_fail(property && *property, NULL);
 
+    nm_assert(!match_data || !device);
+    nm_assert(!device || NM_IS_DEVICE(device));
+
     priv = NM_CONFIG_DATA_GET_PRIVATE(self);
 
     connection_info = _match_section_infos_lookup(&priv->device_infos[0],
                                                   priv->keyfile,
                                                   property,
+                                                  match_data,
                                                   device,
-                                                  NULL,
-                                                  NULL,
                                                   &value);
     NM_SET_OUT(has_match, !!connection_info);
     return value;
 }
 
 const char *
+nm_config_data_get_device_config(const NMConfigData          *self,
+                                 const char                  *property,
+                                 const NMMatchSpecDeviceData *match_data,
+                                 gboolean                    *has_match)
+{
+    return _config_data_get_device_config(self, property, match_data, NULL, has_match);
+}
+
+const char *
+nm_config_data_get_device_config_by_device(const NMConfigData *self,
+                                           const char         *property,
+                                           NMDevice           *device,
+                                           gboolean           *has_match)
+{
+    return _config_data_get_device_config(self, property, NULL, device, has_match);
+}
+
+const char *
 nm_config_data_get_device_config_by_pllink(const NMConfigData   *self,
                                            const char           *property,
                                            const NMPlatformLink *pllink,
@@ -1581,53 +1660,58 @@ nm_config_data_get_device_config_by_pllink(const NMConfigData   *self,
     const NMConfigDataPrivate *priv;
     const MatchSectionInfo    *connection_info;
     const char                *value;
+    NMMatchSpecDeviceData      match_data;
 
     g_return_val_if_fail(self, NULL);
     g_return_val_if_fail(property && *property, NULL);
 
     priv = NM_CONFIG_DATA_GET_PRIVATE(self);
 
+    nm_match_spec_device_data_init_from_platform(&match_data,
+                                                 pllink,
+                                                 match_device_type,
+                                                 nm_dhcp_manager_get_config(nm_dhcp_manager_get()));
+
     connection_info = _match_section_infos_lookup(&priv->device_infos[0],
                                                   priv->keyfile,
                                                   property,
+                                                  &match_data,
                                                   NULL,
-                                                  pllink,
-                                                  match_device_type,
                                                   &value);
     NM_SET_OUT(has_match, !!connection_info);
     return value;
 }
 
 gboolean
-nm_config_data_get_device_config_boolean(const NMConfigData *self,
-                                         const char         *property,
-                                         NMDevice           *device,
-                                         int                 val_no_match,
-                                         int                 val_invalid)
+nm_config_data_get_device_config_boolean_by_device(const NMConfigData *self,
+                                                   const char         *property,
+                                                   NMDevice           *device,
+                                                   int                 val_no_match,
+                                                   int                 val_invalid)
 {
     const char *value;
     gboolean    has_match;
 
-    value = nm_config_data_get_device_config(self, property, device, &has_match);
+    value = nm_config_data_get_device_config_by_device(self, property, device, &has_match);
     if (!has_match)
         return val_no_match;
     return nm_config_parse_boolean(value, val_invalid);
 }
 
 gint64
-nm_config_data_get_device_config_int64(const NMConfigData *self,
-                                       const char         *property,
-                                       NMDevice           *device,
-                                       int                 base,
-                                       gint64              min,
-                                       gint64              max,
-                                       gint64              val_no_match,
-                                       gint64              val_invalid)
+nm_config_data_get_device_config_int64_by_device(const NMConfigData *self,
+                                                 const char         *property,
+                                                 NMDevice           *device,
+                                                 int                 base,
+                                                 gint64              min,
+                                                 gint64              max,
+                                                 gint64              val_no_match,
+                                                 gint64              val_invalid)
 {
     const char *value;
     gboolean    has_match;
 
-    value = nm_config_data_get_device_config(self, property, device, &has_match);
+    value = nm_config_data_get_device_config_by_device(self, property, device, &has_match);
     if (!has_match) {
         errno = ENOENT;
         return val_no_match;
@@ -1651,9 +1735,8 @@ nm_config_data_get_device_allowed_connections_specs(const NMConfigData *self,
     connection_info = _match_section_infos_lookup(&priv->device_infos[0],
                                                   priv->keyfile,
                                                   NM_CONFIG_KEYFILE_KEY_DEVICE_ALLOWED_CONNECTIONS,
-                                                  device,
-                                                  NULL,
                                                   NULL,
+                                                  device,
                                                   NULL);
 
     if (connection_info) {
@@ -1696,9 +1779,8 @@ nm_config_data_get_connection_default(const NMConfigData *self,
     _match_section_infos_lookup(&priv->connection_infos[0],
                                 priv->keyfile,
                                 property,
-                                device,
-                                NULL,
                                 NULL,
+                                device,
                                 &value);
     return value;
 }
diff --git a/src/core/nm-config-data.h b/src/core/nm-config-data.h
index e3dc90dd..9e7a50fc 100644
--- a/src/core/nm-config-data.h
+++ b/src/core/nm-config-data.h
@@ -185,7 +185,11 @@ const char *nm_config_data_get_dns_mode(const NMConfigData *self);
 const char *nm_config_data_get_rc_manager(const NMConfigData *self);
 gboolean    nm_config_data_get_systemd_resolved(const NMConfigData *self);
 
-gboolean nm_config_data_get_ignore_carrier(const NMConfigData *self, NMDevice *device);
+gboolean nm_config_data_get_ignore_carrier_for_port(const NMConfigData *self,
+                                                    const char         *master,
+                                                    const char         *slave_type);
+
+gboolean nm_config_data_get_ignore_carrier_by_device(const NMConfigData *self, NMDevice *device);
 gboolean nm_config_data_get_assume_ipv6ll_only(const NMConfigData *self, NMDevice *device);
 int      nm_config_data_get_sriov_num_vfs(const NMConfigData *self, NMDevice *device);
 
@@ -219,10 +223,17 @@ gint64 nm_config_data_get_connection_default_int64(const NMConfigData *self,
                                                    gint64              max,
                                                    gint64              fallback);
 
-const char *nm_config_data_get_device_config(const NMConfigData *self,
-                                             const char         *property,
-                                             NMDevice           *device,
-                                             gboolean           *has_match);
+struct _NMMatchSpecDeviceData;
+
+const char *nm_config_data_get_device_config(const NMConfigData                  *self,
+                                             const char                          *property,
+                                             const struct _NMMatchSpecDeviceData *match_data,
+                                             gboolean                            *has_match);
+
+const char *nm_config_data_get_device_config_by_device(const NMConfigData *self,
+                                                       const char         *property,
+                                                       NMDevice           *device,
+                                                       gboolean           *has_match);
 
 const char *nm_config_data_get_device_config_by_pllink(const NMConfigData   *self,
                                                        const char           *property,
@@ -230,19 +241,19 @@ const char *nm_config_data_get_device_config_by_pllink(const NMConfigData   *sel
                                                        const char           *match_device_type,
                                                        gboolean             *has_match);
 
-gboolean nm_config_data_get_device_config_boolean(const NMConfigData *self,
-                                                  const char         *property,
-                                                  NMDevice           *device,
-                                                  int                 val_no_match,
-                                                  int                 val_invalid);
-gint64   nm_config_data_get_device_config_int64(const NMConfigData *self,
-                                                const char         *property,
-                                                NMDevice           *device,
-                                                int                 base,
-                                                gint64              min,
-                                                gint64              max,
-                                                gint64              val_no_match,
-                                                gint64              val_invalid);
+gboolean nm_config_data_get_device_config_boolean_by_device(const NMConfigData *self,
+                                                            const char         *property,
+                                                            NMDevice           *device,
+                                                            int                 val_no_match,
+                                                            int                 val_invalid);
+gint64   nm_config_data_get_device_config_int64_by_device(const NMConfigData *self,
+                                                          const char         *property,
+                                                          NMDevice           *device,
+                                                          int                 base,
+                                                          gint64              min,
+                                                          gint64              max,
+                                                          gint64              val_no_match,
+                                                          gint64              val_invalid);
 
 const GSList *nm_config_data_get_device_allowed_connections_specs(const NMConfigData *self,
                                                                   NMDevice           *device,
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index b7445362..b8df41b7 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -853,6 +853,7 @@ static const ConfigGroup config_groups[] = {
                              NM_CONFIG_KEYFILE_KEY_MAIN_HOSTNAME_MODE,
                              NM_CONFIG_KEYFILE_KEY_MAIN_IGNORE_CARRIER,
                              NM_CONFIG_KEYFILE_KEY_MAIN_IWD_CONFIG_PATH,
+                             NM_CONFIG_KEYFILE_KEY_MAIN_MIGRATE_IFCFG_RH,
                              NM_CONFIG_KEYFILE_KEY_MAIN_MONITOR_CONNECTION_FILES,
                              NM_CONFIG_KEYFILE_KEY_MAIN_NO_AUTO_DEFAULT,
                              NM_CONFIG_KEYFILE_KEY_MAIN_PLUGINS,
@@ -878,6 +879,7 @@ static const ConfigGroup config_groups[] = {
         .group = NM_CONFIG_KEYFILE_GROUP_KEYFILE,
         .keys  = NM_MAKE_STRV(NM_CONFIG_KEYFILE_KEY_KEYFILE_HOSTNAME,
                              NM_CONFIG_KEYFILE_KEY_KEYFILE_PATH,
+                             NM_CONFIG_KEYFILE_KEY_KEYFILE_RENAME,
                              NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES, ),
     },
     {
@@ -1531,7 +1533,7 @@ nm_config_keyfile_has_global_dns_config(GKeyFile *keyfile, gboolean internal)
  * intern_config_read:
  * @filename: the filename where to store the internal config
  * @keyfile_conf: the merged configuration from user (/etc/NM/NetworkManager.conf).
- * @out_needs_rewrite: (allow-none): whether the read keyfile contains inconsistent
+ * @out_needs_rewrite: (out) (optional): whether the read keyfile contains inconsistent
  *   data (compared to @keyfile_conf). If %TRUE, you might want to rewrite
  *   the file.
  *
@@ -2058,7 +2060,7 @@ nm_config_set_connectivity_check_enabled(NMConfig *self, gboolean enabled)
 /**
  * nm_config_set_values:
  * @self: the NMConfig instance
- * @keyfile_intern_new: (allow-none): the new internal settings to set.
+ * @keyfile_intern_new: (nullable): the new internal settings to set.
  *   If %NULL, it is equal to an empty keyfile.
  * @allow_write: only if %TRUE, allow writing the changes to file. Otherwise,
  *   do the changes in-memory only.
diff --git a/src/core/nm-config.h b/src/core/nm-config.h
index d56770d0..acec8d05 100644
--- a/src/core/nm-config.h
+++ b/src/core/nm-config.h
@@ -150,6 +150,8 @@ extern char *_nm_config_match_env;
 #define NM_CONFIG_DEVICE_STATE_DIR "" NMRUNDIR "/devices"
 
 #define NM_CONFIG_DEFAULT_LOGGING_AUDIT_BOOL (nm_streq("" NM_CONFIG_DEFAULT_LOGGING_AUDIT, "true"))
+#define NM_CONFIG_DEFAULT_MAIN_MIGRATE_IFCFG_RH_BOOL \
+    (nm_streq("" NM_CONFIG_DEFAULT_MAIN_MIGRATE_IFCFG_RH, "true"))
 
 typedef enum {
     NM_CONFIG_DEVICE_STATE_MANAGED_TYPE_UNKNOWN   = -1,
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c
index aaf9d810..92de44f1 100644
--- a/src/core/nm-connectivity.c
+++ b/src/core/nm-connectivity.c
@@ -981,9 +981,7 @@ check_platform_config(NMConnectivity *self,
         return NM_CONNECTIVITY_NONE;
     }
 
-    switch (addr_family) {
-    case AF_INET:
-    {
+    if (NM_IS_IPv4(addr_family)) {
         const NMPlatformIP4Route *route;
         gboolean                  found_global = FALSE;
         NMDedupMultiIter          iter;
@@ -1002,13 +1000,8 @@ check_platform_config(NMConnectivity *self,
             NM_SET_OUT(reason, "no global route configured");
             return NM_CONNECTIVITY_LIMITED;
         }
-        break;
-    }
-    case AF_INET6:
+    } else {
         /* Route scopes aren't meaningful for IPv6 so any route is fine. */
-        break;
-    default:
-        g_return_val_if_reached(FALSE);
     }
 
     NM_SET_OUT(reason, NULL);
@@ -1050,11 +1043,12 @@ nm_connectivity_check_start(NMConnectivity             *self,
     cb_data->concheck.con_config = _con_config_ref(priv->con_config);
 
     if (iface && ifindex > 0 && priv->enabled && priv->uri_valid) {
-        gboolean            has_systemd_resolved;
-        NMConnectivityState state;
-        const char         *reason;
+        gboolean has_systemd_resolved;
 
         if (platform) {
+            const char         *reason;
+            NMConnectivityState state;
+
             state = check_platform_config(self, platform, ifindex, addr_family, &reason);
             nm_assert((state == NM_CONNECTIVITY_UNKNOWN) == !reason);
             if (state != NM_CONNECTIVITY_UNKNOWN) {
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 28d9a788..5442efbf 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -468,7 +468,7 @@ _kc_invoke_callback(pid_t                   pid,
  * @log_name: for logging, the name of the processes to kill
  * @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
  * to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter is ignored.
- * @callback: (allow-none): callback after the child terminated. This function will always
+ * @callback: (nullable): callback after the child terminated. This function will always
  *   be invoked asynchronously.
  * @user_data: passed on to callback
  *
@@ -507,16 +507,13 @@ nm_utils_kill_child_async(pid_t                   pid,
         return;
     } else if (ret != 0) {
         errsv = errno;
-        /* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
-        if (errsv != ECHILD) {
-            nm_log_err(LOGD_CORE | log_domain,
-                       LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
-                       LOG_NAME_ARGS,
-                       nm_strerror_native(errsv),
-                       errsv);
-            _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
-            return;
-        }
+        nm_log_err(LOGD_CORE | log_domain,
+                   LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
+                   LOG_NAME_ARGS,
+                   nm_strerror_native(errsv),
+                   errsv);
+        _kc_invoke_callback(pid, log_domain, log_name, callback, user_data, FALSE, -1);
+        return;
     }
 
     /* send the first signal. */
@@ -601,7 +598,7 @@ _sleep_duration_convert_ms_to_us(guint32 sleep_duration_msec)
  * @log_domain: log debug information for this domain. Errors and warnings are logged both
  * as %LOGD_CORE and @log_domain.
  * @log_name: name of the process to kill for logging.
- * @child_status: (out) (allow-none): return the exit status of the child, if no error occurred.
+ * @child_status: (out) (optional): return the exit status of the child, if no error occurred.
  * @wait_before_kill_msec: Waittime in milliseconds before sending %SIGKILL signal. Set this value
  * to zero, not to send %SIGKILL. If @sig is already %SIGKILL, this parameter has not effect.
  * @sleep_duration_msec: the synchronous function sleeps repeatedly waiting for the child to terminate.
@@ -647,15 +644,12 @@ nm_utils_kill_child_sync(pid_t       pid,
         goto out;
     } else if (ret != 0) {
         errsv = errno;
-        /* ECHILD means, the process is not a child/does not exist or it has SIGCHILD blocked. */
-        if (errsv != ECHILD) {
-            nm_log_err(LOGD_CORE | log_domain,
-                       LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
-                       LOG_NAME_ARGS,
-                       nm_strerror_native(errsv),
-                       errsv);
-            goto out;
-        }
+        nm_log_err(LOGD_CORE | log_domain,
+                   LOG_NAME_FMT ": unexpected error while waitpid: %s (%d)",
+                   LOG_NAME_ARGS,
+                   nm_strerror_native(errsv),
+                   errsv);
+        goto out;
     }
 
     /* send first signal @sig */
@@ -1154,25 +1148,26 @@ nm_utils_read_link_absolute(const char *link_file, GError **error)
 #define MATCH_TAG_CONFIG_ENV            "env:"
 
 typedef struct {
-    const char *interface_name;
-    const char *device_type;
-    const char *driver;
-    const char *driver_version;
-    const char *dhcp_plugin;
+    /* This struct contains pre-processed data from NMMatchSpecDeviceData so
+     * we only need to parse it once. */
+    const NMMatchSpecDeviceData *data;
+    const char                  *device_type;
+    const char                  *driver;
+    const char                  *driver_version;
+    const char                  *dhcp_plugin;
     struct {
-        const char *value;
-        gboolean    is_parsed;
-        guint       len;
-        guint8      bin[_NM_UTILS_HWADDR_LEN_MAX];
+        gboolean is_parsed;
+        guint    len;
+        guint8   bin[_NM_UTILS_HWADDR_LEN_MAX];
     } hwaddr;
     struct {
-        const char *value;
-        gboolean    is_parsed;
-        guint32     a;
-        guint32     b;
-        guint32     c;
+        gboolean is_parsed;
+        gboolean is_good;
+        guint32  a;
+        guint32  b;
+        guint32  c;
     } s390_subchannels;
-} MatchDeviceData;
+} MatchSpecDeviceData;
 
 static gboolean
 match_device_s390_subchannels_parse(const char *s390_subchannels,
@@ -1240,22 +1235,25 @@ match_device_s390_subchannels_parse(const char *s390_subchannels,
 }
 
 static gboolean
-match_data_s390_subchannels_eval(const char *spec_str, MatchDeviceData *match_data)
+match_data_s390_subchannels_eval(const char *spec_str, MatchSpecDeviceData *match_data)
 {
-    guint32 a, b, c;
+    guint32 a;
+    guint32 b;
+    guint32 c;
 
     if (G_UNLIKELY(!match_data->s390_subchannels.is_parsed)) {
+        nm_assert(!match_data->s390_subchannels.is_good);
         match_data->s390_subchannels.is_parsed = TRUE;
 
-        if (!match_data->s390_subchannels.value
-            || !match_device_s390_subchannels_parse(match_data->s390_subchannels.value,
+        if (!match_data->data->s390_subchannels
+            || !match_device_s390_subchannels_parse(match_data->data->s390_subchannels,
                                                     &match_data->s390_subchannels.a,
                                                     &match_data->s390_subchannels.b,
                                                     &match_data->s390_subchannels.c)) {
-            match_data->s390_subchannels.value = NULL;
             return FALSE;
         }
-    } else if (!match_data->s390_subchannels.value)
+        match_data->s390_subchannels.is_good = TRUE;
+    } else if (!match_data->s390_subchannels.is_good)
         return FALSE;
 
     if (!match_device_s390_subchannels_parse(spec_str, &a, &b, &c))
@@ -1265,15 +1263,16 @@ match_data_s390_subchannels_eval(const char *spec_str, MatchDeviceData *match_da
 }
 
 static gboolean
-match_device_hwaddr_eval(const char *spec_str, MatchDeviceData *match_data)
+match_device_hwaddr_eval(const char *spec_str, MatchSpecDeviceData *match_data)
 {
     if (G_UNLIKELY(!match_data->hwaddr.is_parsed)) {
         match_data->hwaddr.is_parsed = TRUE;
+        nm_assert(match_data->hwaddr.len == 0);
 
-        if (match_data->hwaddr.value) {
+        if (match_data->data->hwaddr) {
             gsize l;
 
-            if (!_nm_utils_hwaddr_aton(match_data->hwaddr.value,
+            if (!_nm_utils_hwaddr_aton(match_data->data->hwaddr,
                                        match_data->hwaddr.bin,
                                        sizeof(match_data->hwaddr.bin),
                                        &l))
@@ -1281,7 +1280,7 @@ match_device_hwaddr_eval(const char *spec_str, MatchDeviceData *match_data)
             match_data->hwaddr.len = l;
         } else
             return FALSE;
-    } else if (!match_data->hwaddr.len)
+    } else if (match_data->hwaddr.len == 0)
         return FALSE;
 
     return nm_utils_hwaddr_matches(spec_str, -1, match_data->hwaddr.bin, match_data->hwaddr.len);
@@ -1336,7 +1335,7 @@ match_except(const char *spec_str, gboolean *out_except)
 }
 
 static gboolean
-match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchDeviceData *match_data)
+match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchSpecDeviceData *match_data)
 {
     if (spec_str[0] == '*' && spec_str[1] == '\0')
         return TRUE;
@@ -1359,10 +1358,10 @@ match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchDeviceData *m
             use_pattern = TRUE;
         }
 
-        if (match_data->interface_name) {
-            if (nm_streq(spec_str, match_data->interface_name))
+        if (match_data->data->interface_name) {
+            if (nm_streq(spec_str, match_data->data->interface_name))
                 return TRUE;
-            if (use_pattern && g_pattern_match_simple(spec_str, match_data->interface_name))
+            if (use_pattern && g_pattern_match_simple(spec_str, match_data->data->interface_name))
                 return TRUE;
         }
         return FALSE;
@@ -1408,7 +1407,8 @@ match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchDeviceData *m
     if (allow_fuzzy) {
         if (match_device_hwaddr_eval(spec_str, match_data))
             return TRUE;
-        if (match_data->interface_name && nm_streq(spec_str, match_data->interface_name))
+        if (match_data->data->interface_name
+            && nm_streq(spec_str, match_data->data->interface_name))
             return TRUE;
     }
 
@@ -1416,42 +1416,40 @@ match_device_eval(const char *spec_str, gboolean allow_fuzzy, MatchDeviceData *m
 }
 
 NMMatchSpecMatchType
-nm_match_spec_device(const GSList *specs,
-                     const char   *interface_name,
-                     const char   *device_type,
-                     const char   *driver,
-                     const char   *driver_version,
-                     const char   *hwaddr,
-                     const char   *s390_subchannels,
-                     const char   *dhcp_plugin)
-{
-    const GSList   *iter;
-    gboolean        has_match        = FALSE;
-    gboolean        has_match_except = FALSE;
-    gboolean        has_except       = FALSE;
-    gboolean        has_not_except   = FALSE;
-    const char     *spec_str;
-    MatchDeviceData match_data = {
-        .interface_name = interface_name,
-        .device_type    = nm_str_not_empty(device_type),
-        .driver         = nm_str_not_empty(driver),
-        .driver_version = nm_str_not_empty(driver_version),
-        .dhcp_plugin    = nm_str_not_empty(dhcp_plugin),
+nm_match_spec_device(const GSList *specs, const NMMatchSpecDeviceData *data)
+{
+    const GSList       *iter;
+    gboolean            has_match        = FALSE;
+    gboolean            has_match_except = FALSE;
+    gboolean            has_except       = FALSE;
+    gboolean            has_not_except   = FALSE;
+    const char         *spec_str;
+    MatchSpecDeviceData match_data;
+
+    nm_assert(data);
+    nm_assert(!data->hwaddr || nm_utils_hwaddr_valid(data->hwaddr, -1));
+
+    if (!specs)
+        return NM_MATCH_SPEC_NO_MATCH;
+
+    match_data = (MatchSpecDeviceData){
+        .data           = data,
+        .device_type    = nm_str_not_empty(data->device_type),
+        .driver         = nm_str_not_empty(data->driver),
+        .driver_version = nm_str_not_empty(data->driver_version),
+        .dhcp_plugin    = nm_str_not_empty(data->dhcp_plugin),
         .hwaddr =
             {
-                .value = hwaddr,
+                .is_parsed = FALSE,
+                .len       = 0,
             },
         .s390_subchannels =
             {
-                .value = s390_subchannels,
+                .is_parsed = FALSE,
+                .is_good   = FALSE,
             },
     };
 
-    nm_assert(!hwaddr || nm_utils_hwaddr_valid(hwaddr, -1));
-
-    if (!specs)
-        return NM_MATCH_SPEC_NO_MATCH;
-
     for (iter = specs; iter; iter = iter->next) {
         gboolean except;
 
@@ -1484,6 +1482,20 @@ nm_match_spec_device(const GSList *specs,
     return _match_result(has_except, has_not_except, has_match, has_match_except);
 }
 
+int
+nm_match_spec_match_type_to_bool(NMMatchSpecMatchType m, int no_match_value)
+{
+    switch (m) {
+    case NM_MATCH_SPEC_MATCH:
+        return TRUE;
+    case NM_MATCH_SPEC_NEG_MATCH:
+        return FALSE;
+    case NM_MATCH_SPEC_NO_MATCH:
+        return no_match_value;
+    }
+    return nm_assert_unreachable_val(no_match_value);
+}
+
 typedef struct {
     const char *uuid;
     const char *id;
@@ -3366,7 +3378,7 @@ nm_utils_stable_id_generated_complete(const char *stable_id_generated)
 }
 
 static void
-_stable_id_append(GString *str, const char *substitution)
+_stable_id_append(NMStrBuf *str, const char *substitution)
 {
     if (!substitution) {
         /* Would have been nicer to append "=NIL;" to differentiate between
@@ -3375,7 +3387,7 @@ _stable_id_append(GString *str, const char *substitution)
          * Can't do that now, as it would change behavior. */
         substitution = "";
     }
-    g_string_append_printf(str, "=%zu{%s}", strlen(substitution), substitution);
+    nm_str_buf_append_printf(str, "=%zu{%s}", strlen(substitution), substitution);
 }
 
 NMUtilsStableType
@@ -3386,8 +3398,9 @@ nm_utils_stable_id_parse(const char *stable_id,
                          const char *uuid,
                          char      **out_generated)
 {
-    gsize    i, idx_start;
-    GString *str = NULL;
+    nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
+    gsize                    i;
+    gsize                    idx_start;
 
     g_return_val_if_fail(out_generated, NM_UTILS_STABLE_TYPE_RANDOM);
 
@@ -3396,6 +3409,14 @@ nm_utils_stable_id_parse(const char *stable_id,
         return NM_UTILS_STABLE_TYPE_UUID;
     }
 
+    if (nm_streq(stable_id, "default${CONNECTION}")) {
+        /* This changed behavior in 1.44. Explicitly setting "default${CONNECTION}"
+         * the same as the built-in default that we get by not configuring
+         * the property. */
+        *out_generated = NULL;
+        return NM_UTILS_STABLE_TYPE_UUID;
+    }
+
     /* the stable-id allows for some dynamic by performing text-substitutions
      * of ${...} patterns.
      *
@@ -3403,7 +3424,7 @@ nm_utils_stable_id_parse(const char *stable_id,
      * In contrast however, the process is unambiguous so that the resulting
      * effective id differs if:
      *  - the original, untranslated stable-id differs
-     *  - or any of the subsitutions differs.
+     *  - or any of the substitution differs.
      *
      * The reason for that is, for example if you specify "${CONNECTION}" in the
      * stable-id, then the resulting ID should be always(!) unique for this connection.
@@ -3440,28 +3461,26 @@ nm_utils_stable_id_parse(const char *stable_id,
             continue;
         }
 
-#define CHECK_PREFIX(prefix)                                                  \
-    ({                                                                        \
-        gboolean _match = FALSE;                                              \
-                                                                              \
-        if (NM_STR_HAS_PREFIX(&stable_id[i], "" prefix "")) {                 \
-            _match = TRUE;                                                    \
-            if (!str)                                                         \
-                str = g_string_sized_new(256);                                \
-            i += NM_STRLEN(prefix);                                           \
-            g_string_append_len(str, &(stable_id)[idx_start], i - idx_start); \
-            idx_start = i;                                                    \
-        }                                                                     \
-        _match;                                                               \
+#define CHECK_PREFIX(prefix)                                                     \
+    ({                                                                           \
+        gboolean _match = FALSE;                                                 \
+                                                                                 \
+        if (NM_STR_HAS_PREFIX(&stable_id[i], "" prefix "")) {                    \
+            _match = TRUE;                                                       \
+            i += NM_STRLEN(prefix);                                              \
+            nm_str_buf_append_len(&str, &(stable_id)[idx_start], i - idx_start); \
+            idx_start = i;                                                       \
+        }                                                                        \
+        _match;                                                                  \
     })
         if (CHECK_PREFIX("${CONNECTION}"))
-            _stable_id_append(str, uuid);
+            _stable_id_append(&str, uuid);
         else if (CHECK_PREFIX("${BOOT}"))
-            _stable_id_append(str, bootid);
+            _stable_id_append(&str, bootid);
         else if (CHECK_PREFIX("${DEVICE}"))
-            _stable_id_append(str, deviceid);
+            _stable_id_append(&str, deviceid);
         else if (CHECK_PREFIX("${MAC}"))
-            _stable_id_append(str, hwaddr);
+            _stable_id_append(&str, hwaddr);
         else if (g_str_has_prefix(&stable_id[i], "${RANDOM}")) {
             /* RANDOM makes not so much sense for cloned-mac-address
              * as the result is similar to specifying "cloned-mac-address=random".
@@ -3474,8 +3493,6 @@ nm_utils_stable_id_parse(const char *stable_id,
              * by toggling only the stable-id property of the connection.
              * With RANDOM being the most short-lived, ~non-stable~ variant.
              */
-            if (str)
-                g_string_free(str, TRUE);
             *out_generated = NULL;
             return NM_UTILS_STABLE_TYPE_RANDOM;
         } else {
@@ -3494,14 +3511,14 @@ nm_utils_stable_id_parse(const char *stable_id,
     }
 #undef CHECK_PREFIX
 
-    if (!str) {
+    if (str.len == 0) {
         *out_generated = NULL;
         return NM_UTILS_STABLE_TYPE_STABLE_ID;
     }
 
     if (idx_start < i)
-        g_string_append_len(str, &stable_id[idx_start], i - idx_start);
-    *out_generated = g_string_free(str, FALSE);
+        nm_str_buf_append_len(&str, &stable_id[idx_start], i - idx_start);
+    *out_generated = nm_str_buf_finalize(&str, NULL);
     return NM_UTILS_STABLE_TYPE_GENERATED;
 }
 
@@ -4877,25 +4894,25 @@ typedef struct {
     gsize    out_buffer_offset;
 } HelperInfo;
 
-#define _NMLOG_PREFIX_NAME "helper"
-#define _NMLOG_DOMAIN      LOGD_CORE
-#define _NMLOG2(level, info, ...)                                                   \
-    G_STMT_START                                                                    \
-    {                                                                               \
-        if (nm_logging_enabled((level), (_NMLOG_DOMAIN))) {                         \
-            HelperInfo *_info = (info);                                             \
-                                                                                    \
-            _nm_log((level),                                                        \
-                    (_NMLOG_DOMAIN),                                                \
-                    0,                                                              \
-                    NULL,                                                           \
-                    NULL,                                                           \
-                    _NMLOG_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                \
-                                       ",%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
-                    NM_HASH_OBFUSCATE_PTR(_info),                                   \
-                    _info->pid _NM_UTILS_MACRO_REST(__VA_ARGS__));                  \
-        }                                                                           \
-    }                                                                               \
+#define _NMLOG2_PREFIX_NAME "nm-daemon-helper"
+#define _NMLOG2_DOMAIN      LOGD_CORE
+#define _NMLOG2(level, info, ...)                                                    \
+    G_STMT_START                                                                     \
+    {                                                                                \
+        if (nm_logging_enabled((level), (_NMLOG2_DOMAIN))) {                         \
+            HelperInfo *_info = (info);                                              \
+                                                                                     \
+            _nm_log((level),                                                         \
+                    (_NMLOG2_DOMAIN),                                                \
+                    0,                                                               \
+                    NULL,                                                            \
+                    NULL,                                                            \
+                    _NMLOG2_PREFIX_NAME "[" NM_HASH_OBFUSCATE_PTR_FMT                \
+                                        ",%d]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+                    NM_HASH_OBFUSCATE_PTR(_info),                                    \
+                    _info->pid _NM_UTILS_MACRO_REST(__VA_ARGS__));                   \
+        }                                                                            \
+    }                                                                                \
     G_STMT_END
 
 static void
@@ -4913,17 +4930,13 @@ helper_info_free(gpointer data)
     nm_clear_g_source_inst(&info->input_source);
     nm_clear_g_source_inst(&info->output_source);
     nm_clear_g_source_inst(&info->error_source);
-
-    if (info->child_stdout != -1)
-        nm_close(info->child_stdout);
-    if (info->child_stdin != -1)
-        nm_close(info->child_stdin);
-    if (info->child_stderr != -1)
-        nm_close(info->child_stderr);
+    nm_clear_fd(&info->child_stdout);
+    nm_clear_fd(&info->child_stdin);
+    nm_clear_fd(&info->child_stderr);
 
     if (info->pid != -1) {
         nm_assert(info->pid > 1);
-        nm_utils_kill_child_async(info->pid, SIGKILL, LOGD_CORE, _NMLOG_PREFIX_NAME, 0, NULL, NULL);
+        nm_utils_kill_child_async(info->pid, SIGKILL, LOGD_CORE, "nm-daemon-helper", 0, NULL, NULL);
     }
 
     g_free(info);
@@ -5015,8 +5028,7 @@ helper_have_data(int fd, GIOCondition condition, gpointer user_data)
         return G_SOURCE_CONTINUE;
 
     nm_clear_g_source_inst(&info->input_source);
-    nm_close(info->child_stdout);
-    info->child_stdout = -1;
+    nm_clear_fd(&info->child_stdout);
 
     _LOG2T(info, "stdout closed");
 
@@ -5044,9 +5056,7 @@ helper_have_err_data(int fd, GIOCondition condition, gpointer user_data)
         return G_SOURCE_CONTINUE;
 
     nm_clear_g_source_inst(&info->error_source);
-    nm_close(info->child_stderr);
-    info->child_stderr = -1;
-
+    nm_clear_fd(&info->child_stderr);
     return G_SOURCE_CONTINUE;
 }
 
@@ -5103,23 +5113,21 @@ nm_utils_spawn_helper(const char *const  *args,
     gs_free_error GError *error    = NULL;
     gs_free char         *commands = NULL;
     HelperInfo           *info;
-    int                   fd_flags;
     const char *const    *arg;
+    GMainContext         *context;
+    gsize                 n;
 
     nm_assert(args && args[0]);
 
     info  = g_new(HelperInfo, 1);
     *info = (HelperInfo){
-        .task         = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
-        .child_stdin  = -1,
-        .child_stdout = -1,
-        .pid          = -1,
+        .task = nm_g_task_new(NULL, cancellable, nm_utils_spawn_helper, callback, cb_data),
     };
 
     if (!g_spawn_async_with_pipes("/",
                                   (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
                                   (char **) NM_MAKE_STRV(),
-                                  G_SPAWN_DO_NOT_REAP_CHILD,
+                                  G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD,
                                   NULL,
                                   NULL,
                                   &info->pid,
@@ -5142,27 +5150,45 @@ nm_utils_spawn_helper(const char *const  *args,
 
     _LOG2D(info, "spawned process with args: %s", (commands = g_strjoinv(" ", (char **) args)));
 
-    info->child_watch_source = g_child_watch_source_new(info->pid);
-    g_source_set_callback(info->child_watch_source,
-                          G_SOURCE_FUNC(helper_child_terminated),
-                          info,
-                          NULL);
-    g_source_attach(info->child_watch_source, g_main_context_get_thread_default());
+    context = g_task_get_context(info->task);
+
+    /* The async function makes a lukewarm attempt to honor the current thread default
+     * context. However, it later uses nm_utils_kill_child_async() which always uses
+     * g_main_context_default(). For now, the function really can only be used with the
+     * main context. */
+    nm_assert(context == g_main_context_default());
+
+    /* We are using a GChildWatchSource in combination with kill()/waitpid()
+     * (where helper_info_free() clears the source and calls
+     * nm_utils_kill_child_async()). That leads to races where glib might have
+     * already reaped the process and our waitpid() call fails with:
+     *
+     *   <error> [TIMESTAMP] kill child process 'nm-daemon-helper' (PID): failed due to unexpected return value -1 by waitpid (No child processes, 10) after sending SIGKILL (9)
+     *
+     * This is a bug in glib, addressed by [1].  Maybe there should be a
+     * workaround here, and not using the child watcher?
+     *
+     * [1] https://gitlab.gnome.org/GNOME/glib/-/merge_requests/3353
+     */
+    info->child_watch_source = nm_g_child_watch_source_new(info->pid,
+                                                           G_PRIORITY_DEFAULT,
+                                                           helper_child_terminated,
+                                                           info,
+                                                           NULL);
+    g_source_attach(info->child_watch_source, context);
 
     info->timeout_source =
         nm_g_timeout_source_new_seconds(20, G_PRIORITY_DEFAULT, helper_timeout, info, NULL);
-    g_source_attach(info->timeout_source, g_main_context_get_thread_default());
+    g_source_attach(info->timeout_source, context);
 
-    /* Set file descriptors as non-blocking */
-    fd_flags = fcntl(info->child_stdin, F_GETFL, 0);
-    fcntl(info->child_stdin, F_SETFL, fd_flags | O_NONBLOCK);
-    fd_flags = fcntl(info->child_stdout, F_GETFL, 0);
-    fcntl(info->child_stdout, F_SETFL, fd_flags | O_NONBLOCK);
-    fd_flags = fcntl(info->child_stderr, F_GETFL, 0);
-    fcntl(info->child_stderr, F_SETFL, fd_flags | O_NONBLOCK);
+    nm_io_fcntl_setfl_update_nonblock(info->child_stdin);
+    nm_io_fcntl_setfl_update_nonblock(info->child_stdout);
+    nm_io_fcntl_setfl_update_nonblock(info->child_stderr);
 
     /* Watch process stdin */
-    info->out_buffer = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, TRUE);
+    for (n = 1, arg = args; *arg; arg++)
+        n += strlen(*arg) + 1u;
+    info->out_buffer = NM_STR_BUF_INIT(n, TRUE);
     for (arg = args; *arg; arg++) {
         nm_str_buf_append(&info->out_buffer, *arg);
         nm_str_buf_append_c(&info->out_buffer, '\0');
@@ -5173,7 +5199,7 @@ nm_utils_spawn_helper(const char *const  *args,
                                                   helper_can_write,
                                                   info,
                                                   NULL);
-    g_source_attach(info->output_source, g_main_context_get_thread_default());
+    g_source_attach(info->output_source, context);
 
     /* Watch process stdout */
     info->in_buffer    = NM_STR_BUF_INIT(0, FALSE);
@@ -5183,7 +5209,7 @@ nm_utils_spawn_helper(const char *const  *args,
                                                  helper_have_data,
                                                  info,
                                                  NULL);
-    g_source_attach(info->input_source, g_main_context_get_thread_default());
+    g_source_attach(info->input_source, context);
 
     /* Watch process stderr */
     info->err_buffer   = NM_STR_BUF_INIT(0, FALSE);
@@ -5193,7 +5219,7 @@ nm_utils_spawn_helper(const char *const  *args,
                                                  helper_have_err_data,
                                                  info,
                                                  NULL);
-    g_source_attach(info->error_source, g_main_context_get_thread_default());
+    g_source_attach(info->error_source, context);
 
     if (cancellable) {
         gulong signal_id;
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index bc936496..55112504 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -193,14 +193,20 @@ typedef enum {
     NM_MATCH_SPEC_NEG_MATCH = 2,
 } NMMatchSpecMatchType;
 
-NMMatchSpecMatchType nm_match_spec_device(const GSList *specs,
-                                          const char   *interface_name,
-                                          const char   *device_type,
-                                          const char   *driver,
-                                          const char   *driver_version,
-                                          const char   *hwaddr,
-                                          const char   *s390_subchannels,
-                                          const char   *dhcp_plugin);
+int nm_match_spec_match_type_to_bool(NMMatchSpecMatchType m, int no_match_value);
+
+typedef struct _NMMatchSpecDeviceData {
+    const char *interface_name;
+    const char *device_type;
+    const char *driver;
+    const char *driver_version;
+    const char *dhcp_plugin;
+    const char *hwaddr;
+    const char *s390_subchannels;
+} NMMatchSpecDeviceData;
+
+NMMatchSpecMatchType nm_match_spec_device(const GSList *specs, const NMMatchSpecDeviceData *data);
+
 NMMatchSpecMatchType nm_match_spec_config(const GSList *specs, guint nm_version, const char *env);
 GSList              *nm_match_spec_split(const char *value);
 char                *nm_match_spec_join(GSList *specs);
@@ -262,18 +268,17 @@ _nmtst_auto_utils_host_id_context_pop(const char *const *unused)
     nmtst_utils_host_id_pop();
 }
 
-#define _NMTST_UTILS_HOST_ID_CONTEXT(uniq, host_id)                                        \
-    _nm_unused nm_auto(_nmtst_auto_utils_host_id_context_pop) const char *const NM_UNIQ_T( \
-        _host_id_context_,                                                                 \
-        uniq) = ({                                                                         \
-        const gint64 _timestamp_ns = 1631000672;                                           \
-                                                                                           \
-        nmtst_utils_host_id_push((const guint8 *) "" host_id "",                           \
-                                 NM_STRLEN(host_id),                                       \
-                                 TRUE,                                                     \
-                                 &_timestamp_ns);                                          \
-        "" host_id "";                                                                     \
-    })
+#define _NMTST_UTILS_HOST_ID_CONTEXT(uniq, host_id)                      \
+    _nm_unused            nm_auto(_nmtst_auto_utils_host_id_context_pop) \
+        const char *const NM_UNIQ_T(_host_id_context_, uniq) = ({        \
+            const gint64 _timestamp_ns = 1631000672;                     \
+                                                                         \
+            nmtst_utils_host_id_push((const guint8 *) "" host_id "",     \
+                                     NM_STRLEN(host_id),                 \
+                                     TRUE,                               \
+                                     &_timestamp_ns);                    \
+            "" host_id "";                                               \
+        })
 
 #define NMTST_UTILS_HOST_ID_CONTEXT(host_id) _NMTST_UTILS_HOST_ID_CONTEXT(NM_UNIQ, host_id)
 
diff --git a/src/core/nm-dbus-manager.c b/src/core/nm-dbus-manager.c
index af7de8c4..0bde5971 100644
--- a/src/core/nm-dbus-manager.c
+++ b/src/core/nm-dbus-manager.c
@@ -1410,48 +1410,23 @@ nm_dbus_manager_start(NMDBusManager                  *self,
 }
 
 gboolean
-nm_dbus_manager_acquire_bus(NMDBusManager *self, gboolean request_name)
+nm_dbus_manager_request_name_sync(NMDBusManager *self)
 {
     NMDBusManagerPrivate      *priv;
     gs_free_error GError      *error = NULL;
     gs_unref_variant GVariant *ret   = NULL;
     guint32                    result;
-    guint                      registration_id;
 
     g_return_val_if_fail(NM_IS_DBUS_MANAGER(self), FALSE);
 
     priv = NM_DBUS_MANAGER_GET_PRIVATE(self);
 
-    /* Create the D-Bus connection and registering the name synchronously.
-     * That is necessary because we need to exit right away if we can't
-     * acquire the name despite connecting to the bus successfully.
-     * It means that something is gravely broken -- such as another NetworkManager
-     * instance running. */
-    priv->main_dbus_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
-    if (!priv->main_dbus_connection) {
-        _LOGE("cannot connect to D-Bus: %s", error->message);
-        return FALSE;
-    }
-
-    g_dbus_connection_set_exit_on_close(priv->main_dbus_connection, FALSE);
-
-    if (!request_name) {
-        _LOGD("D-Bus connection created");
+    if (priv->objmgr_registration_id == 0) {
+        /* Do nothing. We're presumably in the configure-and-quit mode. */
         return TRUE;
     }
 
-    registration_id = g_dbus_connection_register_object(
-        priv->main_dbus_connection,
-        OBJECT_MANAGER_SERVER_BASE_PATH,
-        NM_UNCONST_PTR(GDBusInterfaceInfo, &interface_info_objmgr),
-        &dbus_vtable_objmgr,
-        self,
-        NULL,
-        &error);
-    if (!registration_id) {
-        _LOGE("failure to register object manager: %s", error->message);
-        return FALSE;
-    }
+    g_return_val_if_fail(G_IS_DBUS_CONNECTION(priv->main_dbus_connection), FALSE);
 
     ret = g_dbus_connection_call_sync(
         priv->main_dbus_connection,
@@ -1465,12 +1440,12 @@ nm_dbus_manager_acquire_bus(NMDBusManager *self, gboolean request_name)
         -1,
         NULL,
         &error);
+
     if (!ret) {
         _LOGE("fatal failure to acquire D-Bus service \"%s"
               ": %s",
               NM_DBUS_SERVICE,
               error->message);
-        g_dbus_connection_unregister_object(priv->main_dbus_connection, registration_id);
         return FALSE;
     }
 
@@ -1479,13 +1454,55 @@ nm_dbus_manager_acquire_bus(NMDBusManager *self, gboolean request_name)
         _LOGE("fatal failure to acquire D-Bus service \"%s\" (%u). Service already taken",
               NM_DBUS_SERVICE,
               (guint) result);
-        g_dbus_connection_unregister_object(priv->main_dbus_connection, registration_id);
+        return FALSE;
+    }
+
+    _LOGI("acquired D-Bus service \"%s\"", NM_DBUS_SERVICE);
+    return TRUE;
+}
+
+gboolean
+nm_dbus_manager_setup(NMDBusManager *self)
+{
+    NMDBusManagerPrivate *priv;
+    gs_free_error GError *error = NULL;
+    guint                 registration_id;
+
+    g_return_val_if_fail(NM_IS_DBUS_MANAGER(self), FALSE);
+
+    priv = NM_DBUS_MANAGER_GET_PRIVATE(self);
+
+    g_return_val_if_fail(!priv->main_dbus_connection, FALSE);
+
+    /* Create the D-Bus connection and registering the name synchronously.
+     * That is necessary because we need to exit right away if we can't
+     * acquire the name despite connecting to the bus successfully.
+     * It means that something is gravely broken -- such as another NetworkManager
+     * instance running. */
+    priv->main_dbus_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
+    if (!priv->main_dbus_connection) {
+        _LOGE("cannot connect to D-Bus: %s", error->message);
+        return FALSE;
+    }
+
+    g_dbus_connection_set_exit_on_close(priv->main_dbus_connection, FALSE);
+
+    registration_id = g_dbus_connection_register_object(
+        priv->main_dbus_connection,
+        OBJECT_MANAGER_SERVER_BASE_PATH,
+        NM_UNCONST_PTR(GDBusInterfaceInfo, &interface_info_objmgr),
+        &dbus_vtable_objmgr,
+        self,
+        NULL,
+        &error);
+    if (!registration_id) {
+        _LOGE("failure to register object manager: %s", error->message);
         return FALSE;
     }
 
     priv->objmgr_registration_id = registration_id;
 
-    _LOGI("acquired D-Bus service \"%s\"", NM_DBUS_SERVICE);
+    _LOGD("D-Bus connection created and ObjectManager object registered");
 
     return TRUE;
 }
diff --git a/src/core/nm-dbus-manager.h b/src/core/nm-dbus-manager.h
index b68161db..078dbdd2 100644
--- a/src/core/nm-dbus-manager.h
+++ b/src/core/nm-dbus-manager.h
@@ -37,7 +37,9 @@ typedef void (*NMDBusManagerSetPropertyHandler)(NMDBusObject
                                                 GVariant                          *value,
                                                 gpointer                           user_data);
 
-gboolean nm_dbus_manager_acquire_bus(NMDBusManager *self, gboolean request_name);
+gboolean nm_dbus_manager_setup(NMDBusManager *self);
+
+gboolean nm_dbus_manager_request_name_sync(NMDBusManager *self);
 
 GDBusConnection *nm_dbus_manager_get_dbus_connection(NMDBusManager *self);
 
diff --git a/src/core/nm-firewall-utils.c b/src/core/nm-firewall-utils.c
index 03f1a9a5..45dab093 100644
--- a/src/core/nm-firewall-utils.c
+++ b/src/core/nm-firewall-utils.c
@@ -74,7 +74,7 @@ _nft_ifname_valid(const char *str)
             return NULL;
         }
     }
-    if (i >= NMP_IFNAMSIZ)
+    if (i >= NM_IFNAMSIZ)
         return NULL;
 
     return str;
@@ -154,10 +154,10 @@ _share_iptables_get_name(gboolean is_iptables_chain, const char *prefix, const c
     nm_str_buf_append(&strbuf, prefix);
 
     ip_iface_len = strlen(ip_iface);
-    G_STATIC_ASSERT_EXPR(NMP_IFNAMSIZ == 16);
-    if (ip_iface_len >= NMP_IFNAMSIZ) {
+    G_STATIC_ASSERT_EXPR(NM_IFNAMSIZ == 16);
+    if (ip_iface_len >= NM_IFNAMSIZ) {
         nm_assert_not_reached();
-        ip_iface_len = NMP_IFNAMSIZ - 1;
+        ip_iface_len = NM_IFNAMSIZ - 1;
     }
 
     if (NM_STRCHAR_ALL(ip_iface,
@@ -763,13 +763,15 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
                            const char        *bond_ifname,
                            const char *const *bond_ifnames_down,
                            const char *const *active_members,
-                           const char *const *previous_members)
+                           const char *const *previous_members,
+                           gboolean           with_counters)
 {
     nm_auto_str_buf NMStrBuf strbuf_table_name =
         NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_32, FALSE);
     nm_auto_str_buf NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
     const char              *table_name;
     gsize                    i;
+    const char *const        s_counter = with_counters ? " counter" : "";
 
     if (NM_MORE_ASSERTS > 10 && active_members) {
         /* No duplicates. We make certain assumptions here, and we don't
@@ -876,9 +878,10 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
             _append(&strbuf,
                     "add rule netdev %s %s pkttype {"
                     " broadcast, multicast "
-                    "} counter drop",
+                    "}%s drop",
                     table_name,
-                    chain_name);
+                    chain_name,
+                    s_counter);
         }
 
         /* OVS SLB rule 2
@@ -905,15 +908,17 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
                 table_name,
                 bond_ifname);
         _append(&strbuf,
-                "add rule netdev %s tx-snoop-source-mac set update ether saddr . vlan id"
-                " timeout 5s @macset-tagged counter return"
+                "add rule netdev %s tx-snoop-source-mac set update ether saddr . vlan id "
+                "timeout 5s @macset-tagged%s return"
                 "", /* tagged */
-                table_name);
+                table_name,
+                s_counter);
         _append(&strbuf,
-                "add rule netdev %s tx-snoop-source-mac set update ether saddr"
-                " timeout 5s @macset-untagged counter"
+                "add rule netdev %s tx-snoop-source-mac set update ether saddr timeout 5s "
+                "@macset-untagged%s"
                 "", /* untagged*/
-                table_name);
+                table_name,
+                s_counter);
 
         _append(&strbuf,
                 "add chain netdev %s rx-drop-looped-packets {"
@@ -921,18 +926,20 @@ nm_firewall_nft_stdio_mlag(gboolean           up,
                 "}",
                 table_name,
                 bond_ifname);
+        _append(
+            &strbuf,
+            "add rule netdev %s rx-drop-looped-packets ether saddr . vlan id @macset-tagged%s drop",
+            table_name,
+            s_counter);
         _append(&strbuf,
-                "add rule netdev %s rx-drop-looped-packets ether saddr . vlan id"
-                " @macset-tagged counter drop",
-                table_name);
-        _append(&strbuf,
-                "add rule netdev %s rx-drop-looped-packets ether type vlan counter return"
+                "add rule netdev %s rx-drop-looped-packets ether type vlan%s return"
                 "", /* avoid looking up tagged packets in untagged table */
-                table_name);
+                table_name,
+                s_counter);
         _append(&strbuf,
-                "add rule netdev %s rx-drop-looped-packets ether saddr @macset-untagged"
-                " counter drop",
-                table_name);
+                "add rule netdev %s rx-drop-looped-packets ether saddr @macset-untagged%s drop",
+                table_name,
+                s_counter);
     }
 
 out:
diff --git a/src/core/nm-firewall-utils.h b/src/core/nm-firewall-utils.h
index ca138ccf..9f13a512 100644
--- a/src/core/nm-firewall-utils.h
+++ b/src/core/nm-firewall-utils.h
@@ -39,6 +39,7 @@ GBytes *nm_firewall_nft_stdio_mlag(gboolean           up,
                                    const char        *bond_ifname,
                                    const char *const *bond_ifnames_down,
                                    const char *const *active_members,
-                                   const char *const *previous_members);
+                                   const char *const *previous_members,
+                                   gboolean           with_counters);
 
 #endif /* __NM_FIREWALL_UTILS_H__ */
diff --git a/src/core/nm-keep-alive.c b/src/core/nm-keep-alive.c
index e147163c..3ab5c36e 100644
--- a/src/core/nm-keep-alive.c
+++ b/src/core/nm-keep-alive.c
@@ -364,7 +364,7 @@ nm_keep_alive_disarm(NMKeepAlive *self)
 
 /**
  * nm_keep_alive_destroy:
- * @self: (allow-none): the #NMKeepAlive instance to destroy.
+ * @self: (nullable): the #NMKeepAlive instance to destroy.
  *
  * This does 3 things in one:
  *
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index d5dedb9c..96274ba9 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -2606,7 +2606,6 @@ nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
                                               int                   addr_family,
                                               guint32               route_table,
                                               guint32               route_metric,
-                                              gboolean              force_commit,
                                               const NML3ConfigData *source)
 {
     const int          IS_IPv4 = NM_IS_IPv4(addr_family);
@@ -2651,7 +2650,6 @@ nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
                                                                                     self->ifindex,
                                                                                     route_table,
                                                                                     route_metric,
-                                                                                    force_commit,
                                                                                     &r_stack.r4);
             if (r)
                 nm_l3_config_data_add_route(self, addr_family, NULL, r);
@@ -2687,13 +2685,12 @@ nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
                 }
 
                 rx.r6 = (NMPlatformIP6Route){
-                    .ifindex        = self->ifindex,
-                    .rt_source      = NM_IP_CONFIG_SOURCE_KERNEL,
-                    .table_coerced  = nm_platform_route_table_coerce(route_table),
-                    .metric         = route_metric,
-                    .network        = *a6,
-                    .plen           = plen,
-                    .r_force_commit = force_commit,
+                    .ifindex       = self->ifindex,
+                    .rt_source     = NM_IP_CONFIG_SOURCE_KERNEL,
+                    .table_coerced = nm_platform_route_table_coerce(route_table),
+                    .metric        = route_metric,
+                    .network       = *a6,
+                    .plen          = plen,
                 };
 
                 nm_platform_ip_route_normalize(addr_family, &rx.rx);
@@ -3199,7 +3196,6 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
             NMPlatformIPXAddress       a;
             NML3ConfigMergeHookResult  hook_result = {
                  .ip4acd_not_ready = NM_OPTION_BOOL_DEFAULT,
-                 .force_commit     = NM_OPTION_BOOL_DEFAULT,
             };
 
 #define _ensure_a()                                       \
@@ -3232,12 +3228,6 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                 a.a4.a_acd_not_ready = (!!hook_result.ip4acd_not_ready);
             }
 
-            if (hook_result.force_commit != NM_OPTION_BOOL_DEFAULT
-                && (!!hook_result.force_commit) != a_src->a_force_commit) {
-                _ensure_a();
-                a.ax.a_force_commit = (!!hook_result.force_commit);
-            }
-
             nm_l3_config_data_add_address_full(self,
                                                addr_family,
                                                a_src == &a.ax ? NULL : obj,
@@ -3257,7 +3247,6 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                 NMPlatformIPXRoute        r;
                 NML3ConfigMergeHookResult hook_result = {
                     .ip4acd_not_ready = NM_OPTION_BOOL_DEFAULT,
-                    .force_commit     = NM_OPTION_BOOL_DEFAULT,
                 };
 
 #define _ensure_r()                                     \
@@ -3283,12 +3272,6 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
                     r.rx.ifindex = self->ifindex;
                 }
 
-                if (hook_result.force_commit != NM_OPTION_BOOL_DEFAULT
-                    && (!!hook_result.force_commit) != r_src->r_force_commit) {
-                    _ensure_r();
-                    r.rx.r_force_commit = (!!hook_result.force_commit);
-                }
-
                 if (!NM_FLAGS_HAS(merge_flags, NM_L3_CONFIG_MERGE_FLAGS_CLONE)) {
                     if (r_src->table_any) {
                         _ensure_r();
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index bfab04d9..80abb00d 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -137,7 +137,6 @@ NML3ConfigData *nm_l3_config_data_new_from_platform(NMDedupMultiIndex        *mu
 
 typedef struct {
     NMOptionBool ip4acd_not_ready;
-    NMOptionBool force_commit;
 } NML3ConfigMergeHookResult;
 
 typedef gboolean (*NML3ConfigMergeHookAddObj)(const NML3ConfigData      *l3cd,
@@ -164,7 +163,6 @@ void nm_l3_config_data_add_dependent_device_routes(NML3ConfigData       *self,
                                                    int                   addr_family,
                                                    guint32               route_table,
                                                    guint32               route_metric,
-                                                   gboolean              force_commit,
                                                    const NML3ConfigData *source);
 
 /*****************************************************************************/
diff --git a/src/core/nm-l3-ipv6ll.c b/src/core/nm-l3-ipv6ll.c
index 0133ebe6..38aa98fc 100644
--- a/src/core/nm-l3-ipv6ll.c
+++ b/src/core/nm-l3-ipv6ll.c
@@ -420,9 +420,7 @@ _lladdr_handle_changed(NML3IPv6LL *self, gboolean force_commit)
                                 NM_DNS_PRIORITY_DEFAULT_NORMAL,
                                 NM_L3_ACD_DEFEND_TYPE_ALWAYS,
                                 0,
-                                /* Even if the address was removed from platform, it must
-                                 * be re-added, hence FORCE_ONCE. */
-                                NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE,
+                                NM_L3CFG_CONFIG_FLAGS_NONE,
                                 NM_L3_CONFIG_MERGE_FLAGS_NONE))
             changed = TRUE;
     } else {
@@ -667,17 +665,13 @@ _nm_l3_ipv6ll_new(NML3Cfg                  *l3cfg,
     };
 
     if (self->addrgen.stable_type == NM_UTILS_STABLE_TYPE_NONE) {
-        char sbuf_token[sizeof(self->addrgen.token.iid) * 3];
+        char sbuf_token[INET6_ADDRSTRLEN];
 
         self->addrgen.token.iid = *token_iid;
         _LOGT("created: l3cfg=" NM_HASH_OBFUSCATE_PTR_FMT ", ifindex=%d, token=%s%s",
               NM_HASH_OBFUSCATE_PTR(l3cfg),
               nm_l3cfg_get_ifindex(l3cfg),
-              nm_utils_bin2hexstr_full(&self->addrgen.token.iid,
-                                       sizeof(self->addrgen.token.iid),
-                                       ':',
-                                       FALSE,
-                                       sbuf_token),
+              nm_utils_inet6_interface_identifier_to_token(&self->addrgen.token.iid, sbuf_token),
               self->assume ? ", assume" : "");
     } else {
         self->addrgen.stable_privacy.ifname     = g_strdup(ifname);
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index a49654fe..3c2d3ec8 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -11,6 +11,7 @@
 #include <linux/if_ether.h>
 #include <linux/rtnetlink.h>
 
+#include "libnm-glib-aux/nm-prioq.h"
 #include "libnm-glib-aux/nm-time-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "libnm-platform/nmp-object.h"
@@ -123,25 +124,34 @@ typedef struct {
 
     CList os_lst;
 
-    /* If we have a timeout pending, we link the instance to
-     * self->priv.p->obj_state_temporary_not_available_lst_head. */
-    CList os_temporary_not_available_lst;
-
     /* If a NMPObject is no longer to be configured (but was configured
      * during a previous commit), then we need to remember it so that the
      * next commit can delete the address/route in kernel. It becomes a zombie. */
     CList os_zombie_lst;
 
-    /* We might want to configure "obj" in platform, but it's currently not possible.
-     * For example, certain IPv6 routes can only be added after the IPv6 address
-     * becomes non-tentative (*sigh*). In such a case, we need to remember that, and
-     * retry later. If this timestamp is set to a non-zero value, then it means
-     * we tried to configure the obj (at that timestamp) and failed, but we are
-     * waiting to retry.
+    /* Used by _handle_routes_failed() mechanism. If "os_plobj" is set, then
+     * this is meaningless but should be set to zero.
+     *
+     * If set to a non-zero value, this means adding the object failed.  Until
+     * "os_failedobj_expiry_msec" we are still waiting whether we would be able to
+     * configure the object. Afterwards, we consider the element failed.
      *
-     * See also self->priv.p->obj_state_temporary_not_available_lst_head
-     * and self->priv.p->obj_state_temporary_not_available_timeout_source. */
-    gint64 os_temporary_not_available_timestamp_msec;
+     * Depending on "os_failedobj_prioq_idx", we are currently waiting whether the
+     * condition can resolve itself or becomes a failure. */
+    gint64 os_failedobj_expiry_msec;
+
+    /* The index into the "priv->failedobj_prioq" queue for objects that are failed.
+     * - this field is meaningless in case "os_plobj" is set (but it should be
+     *   set to NM_PRIOQ_IDX_NULL).
+     * - otherwise, if "os_failedobj_expiry_msec" is 0, no error was detected so
+     *   far. The index should be set to NM_PRIOQ_IDX_NULL.
+     * - otherwise, if the index is NM_PRIOQ_IDX_NULL it means that the object
+     *   is not tracked by the queue, no grace timer is pending, and the object
+     *   is considered failed.
+     * - otherwise, the index is used for tracking the element in the queue.
+     *   It means, we are currently waiting to decide whether this will be a
+     *   failure or not. */
+    guint os_failedobj_prioq_idx;
 
     /* When the obj is a zombie (that means, it was previously configured by NML3Cfg, but
      * now no longer), it needs to be deleted from platform. This ratelimits the time
@@ -206,7 +216,6 @@ typedef struct {
     guint32           acd_timeout_msec_confdata;
     NML3AcdDefendType acd_defend_type_confdata : 3;
     bool              dirty_confdata : 1;
-    gboolean          force_commit_once : 1;
 } L3ConfigData;
 
 struct _NML3CfgBlockHandle {
@@ -241,7 +250,6 @@ typedef struct _NML3CfgPrivate {
 
     CList obj_state_lst_head;
     CList obj_state_zombie_lst_head;
-    CList obj_state_temporary_not_available_lst_head;
 
     GHashTable *acd_ipv4_addresses_on_link;
 
@@ -288,12 +296,22 @@ typedef struct _NML3CfgPrivate {
 
     guint64 pseudo_timestamp_counter;
 
-    GSource *obj_state_temporary_not_available_timeout_source;
+    NMPrioq  failedobj_prioq;
+    GSource *failedobj_timeout_source;
+    gint64   failedobj_timeout_expiry_msec;
 
     NML3CfgCommitType commit_on_idle_type;
 
     gint8 commit_reentrant_count;
 
+    union {
+        struct {
+            gint8 commit_reentrant_count_ip_address_sync_6;
+            gint8 commit_reentrant_count_ip_address_sync_4;
+        };
+        gint8 commit_reentrant_count_ip_address_sync_x[2];
+    };
+
     /* The value that was set before we touched the sysctl (this only is
      * meaningful if "ip6_privacy_set" is true. At the end, we want to restore
      * this value. */
@@ -340,6 +358,9 @@ G_DEFINE_TYPE(NML3Cfg, nm_l3cfg, G_TYPE_OBJECT)
 
 #define _MPTCP_TAG(self, IS_IPv4) ((gconstpointer) (&(((const char *) (self))[2 + (!(IS_IPv4))])))
 
+#define _NETNS_WATCHER_IP_ADDR_TAG(self, addr_family) \
+    ((gconstpointer) & (((char *) self)[1 + NM_IS_IPv4(addr_family)]))
+
 /*****************************************************************************/
 
 #define _NMLOG_DOMAIN      LOGD_CORE
@@ -410,8 +431,6 @@ static NM_UTILS_ENUM2STR_DEFINE(
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE, "platform-change-on-idle"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_PRE_COMMIT, "pre-commit"),
     NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT, "post-commit"),
-    NM_UTILS_ENUM2STR(NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED,
-                      "routes-temporary-not-available-expired"),
     NM_UTILS_ENUM2STR_IGNORE(_NM_L3_CONFIG_NOTIFY_TYPE_NUM), );
 
 static NM_UTILS_ENUM2STR_DEFINE(_l3_acd_defend_type_to_string,
@@ -754,51 +773,51 @@ _nm_n_acd_data_probe_new(NML3Cfg *self, in_addr_t addr, guint32 timeout_msec, gp
 
 /*****************************************************************************/
 
-#define nm_assert_obj_state(self, obj_state)                                                      \
-    G_STMT_START                                                                                  \
-    {                                                                                             \
-        if (NM_MORE_ASSERTS > 0) {                                                                \
-            const NML3Cfg      *_self      = (self);                                              \
-            const ObjStateData *_obj_state = (obj_state);                                         \
-                                                                                                  \
-            nm_assert(_obj_state);                                                                \
-            nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(_obj_state->obj),                             \
-                                NMP_OBJECT_TYPE_IP4_ADDRESS,                                      \
-                                NMP_OBJECT_TYPE_IP6_ADDRESS,                                      \
-                                NMP_OBJECT_TYPE_IP4_ROUTE,                                        \
-                                NMP_OBJECT_TYPE_IP6_ROUTE));                                      \
-            nm_assert(!_obj_state->os_plobj || _obj_state->os_was_in_platform);                   \
-            nm_assert((_obj_state->os_temporary_not_available_timestamp_msec == 0)                \
-                      == c_list_is_empty(&_obj_state->os_temporary_not_available_lst));           \
-            if (_self) {                                                                          \
-                if (c_list_is_empty(&_obj_state->os_zombie_lst)) {                                \
-                    nm_assert(_self->priv.p->combined_l3cd_commited);                             \
-                                                                                                  \
-                    if (NM_MORE_ASSERTS > 5) {                                                    \
-                        nm_assert(c_list_contains(&_self->priv.p->obj_state_lst_head,             \
-                                                  &_obj_state->os_lst));                          \
-                        nm_assert((_obj_state->os_temporary_not_available_timestamp_msec == 0)    \
-                                  || c_list_contains(                                             \
-                                      &_self->priv.p->obj_state_temporary_not_available_lst_head, \
-                                      &_obj_state->os_temporary_not_available_lst));              \
-                        nm_assert(_obj_state->os_plobj                                            \
-                                  == nm_platform_lookup_obj(_self->priv.platform,                 \
-                                                            NMP_CACHE_ID_TYPE_OBJECT_TYPE,        \
-                                                            _obj_state->obj));                    \
-                        nm_assert(                                                                \
-                            c_list_is_empty(&obj_state->os_zombie_lst)                            \
-                                ? (_obj_state->obj                                                \
-                                   == nm_dedup_multi_entry_get_obj(nm_l3_config_data_lookup_obj(  \
-                                       _self->priv.p->combined_l3cd_commited,                     \
-                                       _obj_state->obj)))                                         \
-                                : (!nm_l3_config_data_lookup_obj(                                 \
-                                    _self->priv.p->combined_l3cd_commited,                        \
-                                    _obj_state->obj)));                                           \
-                    }                                                                             \
-                }                                                                                 \
-            }                                                                                     \
-        }                                                                                         \
-    }                                                                                             \
+#define nm_assert_obj_state(self, obj_state)                                                     \
+    G_STMT_START                                                                                 \
+    {                                                                                            \
+        if (NM_MORE_ASSERTS > 0) {                                                               \
+            const NML3Cfg      *_self      = (self);                                             \
+            const ObjStateData *_obj_state = (obj_state);                                        \
+                                                                                                 \
+            nm_assert(_obj_state);                                                               \
+            nm_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(_obj_state->obj),                            \
+                                NMP_OBJECT_TYPE_IP4_ADDRESS,                                     \
+                                NMP_OBJECT_TYPE_IP6_ADDRESS,                                     \
+                                NMP_OBJECT_TYPE_IP4_ROUTE,                                       \
+                                NMP_OBJECT_TYPE_IP6_ROUTE));                                     \
+            nm_assert(!_obj_state->os_plobj || _obj_state->os_was_in_platform);                  \
+            nm_assert(_obj_state->os_failedobj_expiry_msec != 0                                  \
+                      || _obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);               \
+            nm_assert(_obj_state->os_failedobj_expiry_msec == 0 || !_obj_state->os_plobj);       \
+            nm_assert(_obj_state->os_failedobj_expiry_msec == 0                                  \
+                      || c_list_is_empty(&_obj_state->os_zombie_lst));                           \
+            nm_assert(_obj_state->os_failedobj_expiry_msec == 0 || _obj_state->obj);             \
+            if (_self) {                                                                         \
+                if (c_list_is_empty(&_obj_state->os_zombie_lst)) {                               \
+                    nm_assert(_self->priv.p->combined_l3cd_commited);                            \
+                                                                                                 \
+                    if (NM_MORE_ASSERTS > 5) {                                                   \
+                        nm_assert(c_list_contains(&_self->priv.p->obj_state_lst_head,            \
+                                                  &_obj_state->os_lst));                         \
+                        nm_assert(_obj_state->os_plobj                                           \
+                                  == nm_platform_lookup_obj(_self->priv.platform,                \
+                                                            NMP_CACHE_ID_TYPE_OBJECT_TYPE,       \
+                                                            _obj_state->obj));                   \
+                        nm_assert(                                                               \
+                            c_list_is_empty(&obj_state->os_zombie_lst)                           \
+                                ? (_obj_state->obj                                               \
+                                   == nm_dedup_multi_entry_get_obj(nm_l3_config_data_lookup_obj( \
+                                       _self->priv.p->combined_l3cd_commited,                    \
+                                       _obj_state->obj)))                                        \
+                                : (!nm_l3_config_data_lookup_obj(                                \
+                                    _self->priv.p->combined_l3cd_commited,                       \
+                                    _obj_state->obj)));                                          \
+                    }                                                                            \
+                }                                                                                \
+            }                                                                                    \
+        }                                                                                        \
+    }                                                                                            \
     G_STMT_END
 
 static ObjStateData *
@@ -808,13 +827,14 @@ _obj_state_data_new(const NMPObject *obj, const NMPObject *plobj)
 
     obj_state  = g_slice_new(ObjStateData);
     *obj_state = (ObjStateData){
-        .obj                            = nmp_object_ref(obj),
-        .os_plobj                       = nmp_object_ref(plobj),
-        .os_was_in_platform             = !!plobj,
-        .os_nm_configured               = FALSE,
-        .os_dirty                       = FALSE,
-        .os_temporary_not_available_lst = C_LIST_INIT(obj_state->os_temporary_not_available_lst),
-        .os_zombie_lst                  = C_LIST_INIT(obj_state->os_zombie_lst),
+        .obj                      = nmp_object_ref(obj),
+        .os_plobj                 = nmp_object_ref(plobj),
+        .os_was_in_platform       = !!plobj,
+        .os_nm_configured         = FALSE,
+        .os_dirty                 = FALSE,
+        .os_failedobj_expiry_msec = 0,
+        .os_failedobj_prioq_idx   = NM_PRIOQ_IDX_NULL,
+        .os_zombie_lst            = C_LIST_INIT(obj_state->os_zombie_lst),
     };
     return obj_state;
 }
@@ -824,9 +844,10 @@ _obj_state_data_free(gpointer data)
 {
     ObjStateData *obj_state = data;
 
+    nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
+
     c_list_unlink_stale(&obj_state->os_lst);
     c_list_unlink_stale(&obj_state->os_zombie_lst);
-    c_list_unlink_stale(&obj_state->os_temporary_not_available_lst);
     nmp_object_unref(obj_state->obj);
     nmp_object_unref(obj_state->os_plobj);
     nm_g_slice_free(obj_state);
@@ -864,15 +885,17 @@ _obj_state_data_to_string(const ObjStateData *obj_state, char *buf, gsize buf_si
     } else if (obj_state->os_was_in_platform)
         nm_strbuf_append_str(&buf, &buf_size, ", was-in-platform");
 
-    if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
+    if (obj_state->os_failedobj_expiry_msec > 0) {
         nm_utils_get_monotonic_timestamp_msec_cached(&now_msec);
-        nm_strbuf_append(
-            &buf,
-            &buf_size,
-            ", temporary-not-available-since=%" G_GINT64_FORMAT ".%03d",
-            (now_msec - obj_state->os_temporary_not_available_timestamp_msec) / 1000,
-            (int) ((now_msec - obj_state->os_temporary_not_available_timestamp_msec) % 1000));
-    }
+        nm_strbuf_append(&buf,
+                         &buf_size,
+                         ", %s-since=%" G_GINT64_FORMAT ".%03d",
+                         (obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL) ? "failed"
+                                                                                  : "failed-wait",
+                         (obj_state->os_failedobj_expiry_msec - now_msec) / 1000,
+                         (int) ((obj_state->os_failedobj_expiry_msec - now_msec) % 1000));
+    } else
+        nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
 
     return buf0;
 }
@@ -934,6 +957,7 @@ _obj_states_externally_removed_track(NML3Cfg *self, const NMPObject *obj, gboole
 
     if (!in_platform && !c_list_is_empty(&obj_state->os_zombie_lst)) {
         /* this is a zombie. We can forget about it.*/
+        nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
         nm_clear_nmp_object(&obj_state->os_plobj);
         c_list_unlink(&obj_state->os_zombie_lst);
         _LOGD("obj-state: zombie gone (untrack): %s",
@@ -949,8 +973,23 @@ _obj_states_externally_removed_track(NML3Cfg *self, const NMPObject *obj, gboole
     if (in_platform) {
         nmp_object_ref_set(&obj_state->os_plobj, obj);
         obj_state->os_was_in_platform = TRUE;
-        _LOGD("obj-state: appeared in platform: %s",
-              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+        if (obj_state->os_failedobj_expiry_msec != 0) {
+            obj_state->os_failedobj_expiry_msec = 0;
+            if (obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL) {
+                _LOGT("obj-state: failed-obj: object now configured after failed earlier: %s",
+                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            } else {
+                nm_prioq_remove(&self->priv.p->failedobj_prioq,
+                                obj_state,
+                                &obj_state->os_failedobj_prioq_idx);
+                _LOGT("obj-state: failed-obj: object now configured after waiting: %s",
+                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            }
+        } else {
+            _LOGD("obj-state: appeared in platform: %s",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+        }
+        nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
         goto out;
     }
 
@@ -1039,6 +1078,7 @@ _obj_states_update_all(NML3Cfg *self)
                 continue;
 
             if (obj_state->os_plobj && obj_state->os_nm_configured) {
+                nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
                 c_list_link_tail(&self->priv.p->obj_state_zombie_lst_head,
                                  &obj_state->os_zombie_lst);
                 obj_state->os_zombie_count = ZOMBIE_COUNT_START;
@@ -1049,6 +1089,9 @@ _obj_states_update_all(NML3Cfg *self)
 
             _LOGD("obj-state: untrack: %s",
                   _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            nm_prioq_remove(&self->priv.p->failedobj_prioq,
+                            obj_state,
+                            &obj_state->os_failedobj_prioq_idx);
             g_hash_table_iter_remove(&h_iter);
         }
     }
@@ -1086,16 +1129,32 @@ _obj_states_sync_filter(NML3Cfg *self, const NMPObject *obj, NML3CfgCommitType c
         return TRUE;
     }
 
-    if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
-        /* we currently try to configure this address (but failed earlier).
-         * Definitely retry. */
-        return TRUE;
-    }
-
-    if (!obj_state->os_plobj && commit_type != NM_L3_CFG_COMMIT_TYPE_REAPPLY
-        && !nmp_object_get_force_commit(obj))
-        return FALSE;
-
+    /* One goal would be that we don't forcefully re-add routes which were
+     * externally removed (e.g. by the user via `ip route del`).
+     *
+     * However,
+     *
+     *  - some routes get automatically deleted by kernel (for example,
+     *  when we have an IPv4 route with RTA_PREFSRC set and the referenced
+     *  IPv4 address gets removed). The absence of such a route does not
+     *  mean that the user doesn't want the route there. It means, kernel
+     *  removed it because of some consistency check, but we want it back.
+     *  - a route with a non-zero gateway requires that the gateway is
+     *  directly reachable via an onlink route. The rules for this are
+     *  complex, but kernel will reject adding a route which has such a
+     *  gateway. If the user manually removed the needed onlink route, the
+     *  gateway route cannot be added in kernel ("Nexthop has invalid
+     *  gateway"). To handle that is a nightmare, so we always ensure that
+     *  the onlink route is there.
+     *  - a route with RTA_PREFSRC requires that such an address is
+     *  configured otherwise kernel rejects adding the route with "Invalid
+     *  prefsrc address"/"Invalid source address".  Removing an address can
+     *  thus prevent adding the route, which is a problem for us.
+     *
+     * So the goal is not tenable and causes problems. NetworkManager will
+     * try hard to re-add routes and address that it thinks should be
+     * present. If you externally remove them, then you are starting a
+     * fight where NetworkManager tries to re-add them on every commit. */
     return TRUE;
 }
 
@@ -1129,6 +1188,7 @@ static void
 _commit_collect_routes(NML3Cfg          *self,
                        int               addr_family,
                        NML3CfgCommitType commit_type,
+                       gboolean          any_addrs,
                        GPtrArray       **routes,
                        GPtrArray       **routes_nodev)
 {
@@ -1154,6 +1214,24 @@ _commit_collect_routes(NML3Cfg          *self,
         else {
             nm_assert(NMP_OBJECT_CAST_IP_ROUTE(obj)->ifindex == self->priv.ifindex);
 
+            if (!any_addrs) {
+                /* This is a unicast route (or a similar route, which has an
+                 * ifindex).
+                 *
+                 * However, during this commit we don't plan to configure any
+                 * IP addresses.  With `ipvx.method=manual` that should not be
+                 * possible. More likely, this is because the profile has
+                 * `ipvx.method=auto` and static routes.
+                 *
+                 * Don't configure any such routes before we also have at least
+                 * one IP address.
+                 *
+                 * This code applies to IPv4 and IPv6, however for IPv6 we
+                 * early on configure a link local address, so in practice the
+                 * branch is not taken for IPv6. */
+                continue;
+            }
+
             if (IS_IPv4 && NMP_OBJECT_CAST_IP4_ROUTE(obj)->weight > 0) {
                 /* This route needs to be registered as ECMP route. */
                 nm_netns_ip_route_ecmp_register(self->priv.netns, self, obj);
@@ -1246,6 +1324,7 @@ _obj_state_zombie_lst_get_prune_lists(NML3Cfg    *self,
         if (--obj_state->os_zombie_count == 0) {
             _LOGD("obj-state: prune zombie (untrack): %s",
                   _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
             g_hash_table_remove(self->priv.p->obj_state_hash, obj_state);
             continue;
         }
@@ -1280,6 +1359,7 @@ _obj_state_zombie_lst_prune_all(NML3Cfg *self, int addr_family)
         if (--obj_state->os_zombie_count == 0) {
             _LOGD("obj-state: zombie pruned during reapply (untrack): %s",
                   _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            nm_assert(obj_state->os_failedobj_prioq_idx == NM_PRIOQ_IDX_NULL);
             g_hash_table_remove(self->priv.p->obj_state_hash, obj_state);
             continue;
         }
@@ -3017,16 +3097,14 @@ nm_l3cfg_get_acd_addr_info(NML3Cfg *self, in_addr_t addr)
 /*****************************************************************************/
 
 gboolean
-nm_l3cfg_has_temp_not_available_obj(NML3Cfg *self, int addr_family)
+nm_l3cfg_has_failedobj_pending(NML3Cfg *self, int addr_family)
 {
     ObjStateData *obj_state;
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert_addr_family(addr_family);
 
-    c_list_for_each_entry (obj_state,
-                           &self->priv.p->obj_state_temporary_not_available_lst_head,
-                           os_temporary_not_available_lst) {
+    nm_prioq_for_each (&self->priv.p->failedobj_prioq, obj_state) {
         if (NMP_OBJECT_GET_ADDR_FAMILY(obj_state->obj) == addr_family)
             return TRUE;
     }
@@ -3413,8 +3491,7 @@ nm_l3cfg_add_config(NML3Cfg              *self,
             .acd_timeout_msec_confdata = acd_timeout_msec,
             .priority_confdata         = priority,
             .pseudo_timestamp_confdata = ++self->priv.p->pseudo_timestamp_counter,
-            .force_commit_once = NM_FLAGS_HAS(config_flags, NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE),
-            .dirty_confdata    = FALSE,
+            .dirty_confdata            = FALSE,
         };
         changed = TRUE;
     } else {
@@ -3611,7 +3688,6 @@ typedef struct {
     NML3Cfg      *self;
     gconstpointer tag;
     bool          to_commit;
-    bool          force_commit_once;
 } L3ConfigMergeHookAddObjData;
 
 static gboolean
@@ -3629,9 +3705,6 @@ _l3_hook_add_obj_cb(const NML3ConfigData      *l3cd,
     nm_assert(obj);
     nm_assert(hook_result);
     nm_assert(hook_result->ip4acd_not_ready == NM_OPTION_BOOL_DEFAULT);
-    nm_assert(hook_result->force_commit == NM_OPTION_BOOL_DEFAULT);
-
-    hook_result->force_commit = hook_data->force_commit_once;
 
     switch (NMP_OBJECT_GET_TYPE(obj)) {
     case NMP_OBJECT_TYPE_IP4_ADDRESS:
@@ -3787,8 +3860,7 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
             if (NM_FLAGS_HAS(l3cd_data->config_flags, NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD))
                 continue;
 
-            hook_data.tag               = l3cd_data->tag_confdata;
-            hook_data.force_commit_once = l3cd_data->force_commit_once;
+            hook_data.tag = l3cd_data->tag_confdata;
 
             nm_l3_config_data_merge(l3cd,
                                     l3cd_data->l3cd,
@@ -3846,7 +3918,6 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
                     IS_IPv4 ? AF_INET : AF_INET6,
                     l3cd_data->default_route_table_x[IS_IPv4],
                     l3cd_data->default_route_metric_x[IS_IPv4],
-                    l3cd_data->force_commit_once,
                     l3cd_data->l3cd);
             }
         }
@@ -3921,79 +3992,91 @@ out:
 /*****************************************************************************/
 
 static gboolean
-_routes_temporary_not_available_timeout(gpointer user_data)
+_failedobj_timeout_cb(gpointer user_data)
 {
-    NML3Cfg      *self = NM_L3CFG(user_data);
-    ObjStateData *obj_state;
-    gint64        now_msec;
-    gint64        expiry_msec;
+    NML3Cfg *self = NM_L3CFG(user_data);
 
-    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
+    _LOGT("obj-state: failed-obj: handle timeout");
 
-    obj_state = c_list_first_entry(&self->priv.p->obj_state_temporary_not_available_lst_head,
-                                   ObjStateData,
-                                   os_temporary_not_available_lst);
+    nm_clear_g_source_inst(&self->priv.p->failedobj_timeout_source);
 
-    if (!obj_state)
-        return G_SOURCE_CONTINUE;
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
 
-    now_msec = nm_utils_get_monotonic_timestamp_msec();
+    return G_SOURCE_CONTINUE;
+}
 
-    expiry_msec = obj_state->os_temporary_not_available_timestamp_msec
-                  + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC;
+static void
+_failedobj_reschedule(NML3Cfg *self, gint64 now_msec)
+{
+    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    ObjStateData *obj_state;
 
-    if (now_msec < expiry_msec) {
-        /* the timeout is not yet reached. Restart the timer... */
-        self->priv.p->obj_state_temporary_not_available_timeout_source =
-            nm_g_timeout_add_source(expiry_msec - now_msec,
-                                    _routes_temporary_not_available_timeout,
-                                    self);
-        return G_SOURCE_CONTINUE;
+    nm_utils_get_monotonic_timestamp_msec_cached(&now_msec);
+
+again:
+    obj_state = nm_prioq_peek(&self->priv.p->failedobj_prioq);
+
+    if (obj_state && obj_state->os_failedobj_expiry_msec <= now_msec) {
+        /* The object is already expired... */
+
+        /* we shouldn't have a "os_plobj", because if we had, we should have
+         * removed "obj_state" from the queue. */
+        nm_assert(!obj_state->os_plobj);
+
+        /* we need to have an "obj", otherwise the "obj_state" instance
+         * shouldn't exist (as it also has not "os_plobj"). */
+        nm_assert(obj_state->obj);
+
+        /* It seems that nm_platform_ip_route_sync() signaled success and did
+         * not report the route as missing. Regardless, it is still not
+         * configured and the timeout expired. */
+        nm_prioq_remove(&self->priv.p->failedobj_prioq,
+                        obj_state,
+                        &obj_state->os_failedobj_prioq_idx);
+        _LOGW(
+            "missing IPv%c route: %s",
+            nm_utils_addr_family_to_char(NMP_OBJECT_GET_TYPE(obj_state->obj)),
+            nmp_object_to_string(obj_state->obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+        goto again;
     }
 
-    /* One (or several) routes expired. We emit a signal, but we don't schedule it again.
-     * We expect the callers to commit again, which will one last time try to configure
-     * the route. If that again fails, we detect the timeout, log a warning and don't
-     * track the object as not temporary-not-available anymore. */
-    _nm_l3cfg_emit_signal_notify_simple(
-        self,
-        NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED);
-    return G_SOURCE_CONTINUE;
+    if (!obj_state) {
+        if (nm_clear_g_source_inst(&self->priv.p->failedobj_timeout_source))
+            _LOGT("obj-state: failed-obj: cancel timeout");
+        return;
+    }
+
+    if (nm_g_timeout_reschedule(&self->priv.p->failedobj_timeout_source,
+                                &self->priv.p->failedobj_timeout_expiry_msec,
+                                obj_state->os_failedobj_expiry_msec,
+                                _failedobj_timeout_cb,
+                                self)) {
+        _LOGT(
+            "obj-state: failed-obj: schedule timeout in %" G_GINT64_FORMAT " msec",
+            NM_MAX((gint64) 0,
+                   obj_state->os_failedobj_expiry_msec - nm_utils_get_monotonic_timestamp_msec()));
+    }
 }
 
-static gboolean
-_routes_temporary_not_available_update(NML3Cfg   *self,
-                                       int        addr_family,
-                                       GPtrArray *routes_temporary_not_available_arr)
+static void
+_failedobj_handle_routes(NML3Cfg *self, int addr_family, GPtrArray *routes_failed)
 {
-    ObjStateData   *obj_state;
-    ObjStateData   *obj_state_safe;
-    gint64          now_msec;
-    gboolean        prune_all = FALSE;
-    gboolean        success   = TRUE;
-    guint           i;
-    const NMPClass *klass;
-
-    klass    = nmp_class_from_type(NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)));
-    now_msec = nm_utils_get_monotonic_timestamp_msec();
-
-    if (nm_g_ptr_array_len(routes_temporary_not_available_arr) <= 0) {
-        prune_all = TRUE;
-        goto out_prune;
-    }
+    const gint64  now_msec = nm_utils_get_monotonic_timestamp_msec();
+    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    ObjStateData *obj_state;
+    guint         i;
 
-    c_list_for_each_entry (obj_state,
-                           &self->priv.p->obj_state_temporary_not_available_lst_head,
-                           os_temporary_not_available_lst) {
-        if (NMP_OBJECT_GET_CLASS(obj_state->obj) == klass) {
-            nm_assert(obj_state->os_temporary_not_available_timestamp_msec > 0);
-            obj_state->os_tna_dirty = TRUE;
-        }
-    }
+    if (!routes_failed)
+        return;
 
-    for (i = 0; i < routes_temporary_not_available_arr->len; i++) {
-        const NMPObject *o = routes_temporary_not_available_arr->pdata[i];
-        char             sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    for (i = 0; i < routes_failed->len; i++) {
+        const NMPObject          *o                    = routes_failed->pdata[i];
+        const NMPlatformIPXRoute *rt                   = NMP_OBJECT_CAST_IPX_ROUTE(o);
+        gboolean                  just_started_to_fail = FALSE;
+        gboolean                  just_failed          = FALSE;
+        gboolean                  arm_timer            = FALSE;
+        int                       grace_timeout_msec;
+        gint64                    grace_expiry_mesc;
 
         nm_assert(NMP_OBJECT_GET_TYPE(o) == NMP_OBJECT_TYPE_IP_ROUTE(NM_IS_IPv4(addr_family)));
 
@@ -4005,70 +4088,83 @@ _routes_temporary_not_available_update(NML3Cfg   *self,
             continue;
         }
 
-        if (obj_state->os_temporary_not_available_timestamp_msec > 0) {
-            nm_assert(obj_state->os_temporary_not_available_timestamp_msec > 0
-                      && obj_state->os_temporary_not_available_timestamp_msec <= now_msec);
-
-            if (!obj_state->os_tna_dirty) {
-                /* Odd, this only can happen if routes_temporary_not_available_arr contains duplicates.
-                 * It should not. */
-                nm_assert_not_reached();
-                continue;
-            }
-
-            if (now_msec > obj_state->os_temporary_not_available_timestamp_msec
-                               + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC) {
-                /* Timeout. Could not add this address.
-                 *
-                 * For now, keep it obj_state->os_tna_dirty and prune it below. */
-                _LOGW("failure to add IPv%c route: %s",
-                      nm_utils_addr_family_to_char(addr_family),
-                      nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-                success = FALSE;
-                continue;
-            }
-
-            obj_state->os_tna_dirty = FALSE;
+        if (obj_state->os_plobj) {
+            /* This object is apparently present in platform. Not sure what this failure report
+             * is about. Probably some harmless glitch. Ignore. */
             continue;
         }
 
-        _LOGT("(temporarily) unable to add IPv%c route: %s",
-              nm_utils_addr_family_to_char(addr_family),
-              nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+        /* This route failed, but why? That determines the grace time that we
+         * give before considering it bad. */
+        if (!nm_ip_addr_is_null(addr_family,
+                                nm_platform_ip_route_get_pref_src(addr_family, &rt->rx))) {
+            /* This route has a pref_src. A common cause for being unable to
+             * configure such routes, is that the referenced IP address is not
+             * configured/ready (yet). Give a longer timeout to this case. */
+            grace_timeout_msec = 10000;
+        } else {
+            /* Other route don't have any grace time. There is no retry/wait,
+             * they are a failure right away. */
+            grace_timeout_msec = 0;
+        }
 
-        obj_state->os_tna_dirty                              = FALSE;
-        obj_state->os_temporary_not_available_timestamp_msec = now_msec;
-        c_list_link_tail(&self->priv.p->obj_state_temporary_not_available_lst_head,
-                         &obj_state->os_temporary_not_available_lst);
-    }
+        grace_expiry_mesc = now_msec + grace_timeout_msec;
 
-out_prune:
-    c_list_for_each_entry_safe (obj_state,
-                                obj_state_safe,
-                                &self->priv.p->obj_state_temporary_not_available_lst_head,
-                                os_temporary_not_available_lst) {
-        if (prune_all || obj_state->os_tna_dirty) {
-            if (NMP_OBJECT_GET_CLASS(obj_state->obj) == klass) {
-                obj_state->os_temporary_not_available_timestamp_msec = 0;
-                c_list_unlink(&obj_state->os_temporary_not_available_lst);
+        if (obj_state->os_failedobj_expiry_msec == 0) {
+            /* This is a new failure that we didn't see before... */
+            obj_state->os_failedobj_expiry_msec = grace_expiry_mesc;
+            if (grace_timeout_msec == 0)
+                just_failed = TRUE;
+            else {
+                arm_timer            = TRUE;
+                just_started_to_fail = TRUE;
             }
+        } else {
+            if (obj_state->os_failedobj_expiry_msec > grace_expiry_mesc) {
+                /* Shorten the grace timeout. We anyway rearm below... */
+                obj_state->os_failedobj_expiry_msec = grace_expiry_mesc;
+            }
+            if (obj_state->os_failedobj_expiry_msec <= now_msec) {
+                /* The grace period is (already) expired. */
+                if (obj_state->os_failedobj_prioq_idx != NM_PRIOQ_IDX_NULL) {
+                    /* We are still tracking the element. It just is about to become failed. */
+                    just_failed = TRUE;
+                }
+            } else
+                arm_timer = TRUE;
         }
-    }
 
-    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
-
-    obj_state = c_list_first_entry(&self->priv.p->obj_state_temporary_not_available_lst_head,
-                                   ObjStateData,
-                                   os_temporary_not_available_lst);
-    if (obj_state) {
-        self->priv.p->obj_state_temporary_not_available_timeout_source =
-            nm_g_timeout_add_source((obj_state->os_temporary_not_available_timestamp_msec
-                                     + ROUTES_TEMPORARY_NOT_AVAILABLE_MAX_AGE_MSEC - now_msec),
-                                    _routes_temporary_not_available_timeout,
-                                    self);
+        nm_prioq_update(&self->priv.p->failedobj_prioq,
+                        obj_state,
+                        &obj_state->os_failedobj_prioq_idx,
+                        arm_timer);
+
+        if (just_failed) {
+            _LOGW("unable to configure IPv%c route: %s",
+                  nm_utils_addr_family_to_char(addr_family),
+                  nmp_object_to_string(o, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
+        } else if (just_started_to_fail) {
+            _LOGT("obj-state: failed-obj: unable to configure %s. Wait for %d msec",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)),
+                  grace_timeout_msec);
+        }
     }
+}
+
+static int
+_failedobj_prioq_cmp(gconstpointer a, gconstpointer b)
+{
+    const ObjStateData *object_state_a = a;
+    const ObjStateData *object_state_b = b;
+
+    nm_assert(object_state_a);
+    nm_assert(object_state_a->os_failedobj_expiry_msec > 0);
+    nm_assert(object_state_b);
+    nm_assert(object_state_b->os_failedobj_expiry_msec > 0);
 
-    return success;
+    NM_CMP_SELF(object_state_a, object_state_b);
+    NM_CMP_FIELD(object_state_a, object_state_b, os_failedobj_expiry_msec);
+    return 0;
 }
 
 /*****************************************************************************/
@@ -4391,6 +4487,147 @@ _rp_filter_update(NML3Cfg *self, gboolean reapply)
 
 /*****************************************************************************/
 
+static void
+_routes_watch_ip_addrs_cb(NMNetns                       *netns,
+                          NMNetnsWatcherType             watcher_type,
+                          const NMNetnsWatcherData      *watcher_data,
+                          gconstpointer                  tag,
+                          const NMNetnsWatcherEventData *event_data,
+                          gpointer                       user_data)
+{
+    const int IS_IPv4 = NM_IS_IPv4(watcher_data->ip_addr.addr.addr_family);
+    NML3Cfg  *self    = user_data;
+    char      sbuf[NM_INET_ADDRSTRLEN];
+
+    if (NMP_OBJECT_CAST_IP_ADDRESS(event_data->ip_addr.obj)->ifindex == self->priv.ifindex) {
+        if (self->priv.p->commit_reentrant_count_ip_address_sync_x[IS_IPv4] > 0) {
+            /* We are currently commiting IP addresses on this very interface.
+             * We can ignore the event. Also, because we will sync the routes
+             * immediately after already.  So even if somebody externally added
+             * the address just this very moment, we would still do the commit
+             * at the right time to ensure our routes are there. */
+            return;
+        }
+    }
+
+    if (event_data->ip_addr.change_type == NM_PLATFORM_SIGNAL_REMOVED)
+        return;
+
+    _LOGT("watched ip-address %s changed. Schedule an idle commit",
+          nm_inet_ntop(watcher_data->ip_addr.addr.addr_family,
+                       &watcher_data->ip_addr.addr.addr,
+                       sbuf));
+    nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
+}
+
+static void
+_routes_watch_ip_addrs(NML3Cfg *self, int addr_family, GPtrArray *addresses, GPtrArray *routes)
+{
+    gconstpointer const TAG          = _NETNS_WATCHER_IP_ADDR_TAG(self, addr_family);
+    NMNetnsWatcherData  watcher_data = {
+         .ip_addr =
+            {
+                 .addr =
+                    {
+                         .addr_family = addr_family,
+                    },
+            },
+    };
+    guint i;
+    guint j;
+
+    /* IP routes that have a pref_src, can only be configured in kernel if that
+     * address exists (and is non-tentative, in case of IPv6).  That address
+     * might be on another interface. So we actually watch all other
+     * interfaces.
+     *
+     * Note that while we track failure to configure routes via "failedobj"
+     * mechanism, we eagerly register watchers, even if the route is already
+     * successfully configured or if the route is to be configure the first
+     * time.  Maybe that could be improved, but
+     * - watchers should be cheap unless they notify the event.
+     * - upon change we do an async commit, which is maybe not entirely cheap
+     *   but cheap enough. More importantly, committing is something that
+     *   *always* should be permissible -- because NML3Cfg has multiple,
+     *   independent users, that don't know about each other and which
+     *   independently are allowed to issue a commit when they think something
+     *   relevant changed. If there are really too many, unnecessary commits,
+     *   then the cause needs to be understood and addressed explicitly. */
+
+    if (!routes)
+        goto out;
+
+    for (i = 0; i < routes->len; i++) {
+        const NMPlatformIPRoute *rt = NMP_OBJECT_CAST_IP_ROUTE(routes->pdata[i]);
+        gconstpointer            pref_src;
+
+        nm_assert(NMP_OBJECT_GET_ADDR_FAMILY(routes->pdata[i]) == addr_family);
+
+        pref_src = nm_platform_ip_route_get_pref_src(addr_family, rt);
+
+        if (nm_ip_addr_is_null(addr_family, pref_src))
+            continue;
+
+        if (NM_IS_IPv4(addr_family)) {
+            if (addresses) {
+                /* This nested loop makes the whole operation O(n*m). We still
+                 * do it that way, because it's probably faster to just iterate
+                 * over the few addresses instead of building a lookup index to
+                 * get it in O(n+m). */
+                for (j = 0; j < addresses->len; j++) {
+                    const NMPlatformIPAddress *a = NMP_OBJECT_CAST_IP_ADDRESS(addresses->pdata[j]);
+
+                    nm_assert(NMP_OBJECT_GET_ADDR_FAMILY(addresses->pdata[j]) == addr_family);
+
+                    if (nm_ip_addr_equal(addr_family, pref_src, a->address_ptr)) {
+                        /* We optimize for the case where the required address
+                         * is about be configured in the same commit. That is a
+                         * common case, because our DHCP routes have prefsrc
+                         * set, and we commonly have the respective IP address
+                         * ready.  Otherwise, the very common DHCP case would
+                         * also require the overhead of registering a watcher
+                         * (every time).
+                         */
+                        goto next;
+                    }
+                }
+            }
+        } else {
+            /* For IPv6, the prefsrc address must also be non-tentative (or
+             * IFA_F_OPTIMISTIC).  So by only looking at the addresses we are
+             * about to configure, it's not clear whether we will be able to
+             * configure the route too.
+             *
+             * Maybe we could check current platform, whether the address
+             * exists there as non-tentative, but that seems fragile.
+             *
+             * Maybe we should only register watchers, after we encountered a
+             * failure to configure a route, but that seems complicated (and
+             * has the potential to be wrong).
+             *
+             * The overhead for always watching the IPv6 address should be
+             * acceptably small. So just do that.
+             */
+        }
+
+        nm_assert(watcher_data.ip_addr.addr.addr_family == addr_family);
+        nm_ip_addr_set(addr_family, &watcher_data.ip_addr.addr.addr, pref_src);
+
+        nm_netns_watcher_add(self->priv.netns,
+                             NM_NETNS_WATCHER_TYPE_IP_ADDR,
+                             &watcher_data,
+                             TAG,
+                             _routes_watch_ip_addrs_cb,
+                             self);
+next:
+        (void) 0;
+    }
+
+out:
+    nm_netns_watcher_remove_all(self->priv.netns, TAG, FALSE);
+}
+/*****************************************************************************/
+
 static gboolean
 _global_tracker_mptcp_untrack(NML3Cfg *self, int addr_family)
 {
@@ -4576,24 +4813,22 @@ _l3_commit_mptcp(NML3Cfg *self, NML3CfgCommitType commit_type)
     _rp_filter_update(self, reapply);
 }
 
-static gboolean
+static void
 _l3_commit_one(NML3Cfg              *self,
                int                   addr_family,
                NML3CfgCommitType     commit_type,
                gboolean              changed_combined_l3cd,
                const NML3ConfigData *l3cd_old)
 {
-    const int                    IS_IPv4                            = NM_IS_IPv4(addr_family);
-    gs_unref_ptrarray GPtrArray *addresses                          = NULL;
-    gs_unref_ptrarray GPtrArray *routes                             = NULL;
-    gs_unref_ptrarray GPtrArray *routes_nodev                       = NULL;
-    gs_unref_ptrarray GPtrArray *addresses_prune                    = NULL;
-    gs_unref_ptrarray GPtrArray *routes_prune                       = NULL;
-    gs_unref_ptrarray GPtrArray *routes_temporary_not_available_arr = NULL;
+    const int                    IS_IPv4         = NM_IS_IPv4(addr_family);
+    gs_unref_ptrarray GPtrArray *addresses       = NULL;
+    gs_unref_ptrarray GPtrArray *routes          = NULL;
+    gs_unref_ptrarray GPtrArray *routes_nodev    = NULL;
+    gs_unref_ptrarray GPtrArray *addresses_prune = NULL;
+    gs_unref_ptrarray GPtrArray *routes_prune    = NULL;
+    gs_unref_ptrarray GPtrArray *routes_failed   = NULL;
     NMIPRouteTableSyncMode       route_table_sync;
-    gboolean                     final_failure_for_temporary_not_available = FALSE;
     char                         sbuf_commit_type[50];
-    gboolean                     success = TRUE;
     guint                        i;
 
     nm_assert(NM_IS_L3CFG(self));
@@ -4609,7 +4844,12 @@ _l3_commit_one(NML3Cfg              *self,
 
     addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
-    _commit_collect_routes(self, addr_family, commit_type, &routes, &routes_nodev);
+    _commit_collect_routes(self,
+                           addr_family,
+                           commit_type,
+                           nm_g_ptr_array_len(addresses) > 0,
+                           &routes,
+                           &routes_nodev);
 
     route_table_sync =
         self->priv.p->combined_l3cd_commited
@@ -4689,9 +4929,14 @@ _l3_commit_one(NML3Cfg              *self,
             }
         }
     }
+
+    _routes_watch_ip_addrs(self, addr_family, addresses, routes);
+
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ndisc_*(). */
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_mtu(). */
 
+    self->priv.p->commit_reentrant_count_ip_address_sync_x[IS_IPv4]++;
+
     nm_platform_ip_address_sync(self->priv.platform,
                                 addr_family,
                                 self->priv.ifindex,
@@ -4701,26 +4946,18 @@ _l3_commit_one(NML3Cfg              *self,
                                     ? NMP_IP_ADDRESS_SYNC_FLAGS_NONE
                                     : NMP_IP_ADDRESS_SYNC_FLAGS_WITH_NOPREFIXROUTE);
 
-    _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
-
-    if (!nm_platform_ip_route_sync(self->priv.platform,
-                                   addr_family,
-                                   self->priv.ifindex,
-                                   routes,
-                                   routes_prune,
-                                   &routes_temporary_not_available_arr))
-        success = FALSE;
+    self->priv.p->commit_reentrant_count_ip_address_sync_x[IS_IPv4]--;
 
-    final_failure_for_temporary_not_available = FALSE;
-    if (!_routes_temporary_not_available_update(self,
-                                                addr_family,
-                                                routes_temporary_not_available_arr))
-        final_failure_for_temporary_not_available = TRUE;
+    _nodev_routes_sync(self, addr_family, commit_type, routes_nodev);
 
-    /* FIXME(l3cfg) */
-    (void) final_failure_for_temporary_not_available;
+    nm_platform_ip_route_sync(self->priv.platform,
+                              addr_family,
+                              self->priv.ifindex,
+                              routes,
+                              routes_prune,
+                              &routes_failed);
 
-    return success;
+    _failedobj_handle_routes(self, addr_family, routes_failed);
 }
 
 static void
@@ -4733,7 +4970,6 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     gboolean                                 is_sticky_update      = FALSE;
     char                                     sbuf_ct[30];
     gboolean                                 changed_combined_l3cd;
-    guint                                    i;
 
     g_return_if_fail(NM_IS_L3CFG(self));
     nm_assert(NM_IN_SET(commit_type,
@@ -4794,19 +5030,12 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     _l3_commit_one(self, AF_INET, commit_type, changed_combined_l3cd, l3cd_old);
     _l3_commit_one(self, AF_INET6, commit_type, changed_combined_l3cd, l3cd_old);
 
+    _failedobj_reschedule(self, 0);
+
     _l3_commit_mptcp(self, commit_type);
 
     _l3_acd_data_process_changes(self);
 
-    if (self->priv.p->l3_config_datas) {
-        for (i = 0; i < self->priv.p->l3_config_datas->len; i++) {
-            L3ConfigData *l3_config_data = _l3_config_datas_at(self->priv.p->l3_config_datas, i);
-
-            if (l3_config_data->force_commit_once)
-                l3_config_data->force_commit_once = FALSE;
-        }
-    }
-
     nm_assert(self->priv.p->commit_reentrant_count == 1);
     self->priv.p->commit_reentrant_count--;
 
@@ -5164,7 +5393,6 @@ nm_l3cfg_init(NML3Cfg *self)
     c_list_init(&self->priv.p->acd_event_notify_lst_head);
     c_list_init(&self->priv.p->commit_type_lst_head);
     c_list_init(&self->priv.p->obj_state_lst_head);
-    c_list_init(&self->priv.p->obj_state_temporary_not_available_lst_head);
     c_list_init(&self->priv.p->obj_state_zombie_lst_head);
     c_list_init(&self->priv.p->blocked_lst_head_4);
     c_list_init(&self->priv.p->blocked_lst_head_6);
@@ -5176,6 +5404,8 @@ nm_l3cfg_init(NML3Cfg *self)
                                                          nmp_object_indirect_id_equal,
                                                          _obj_state_data_free,
                                                          NULL);
+
+    nm_prioq_init(&self->priv.p->failedobj_prioq, _failedobj_prioq_cmp);
 }
 
 static void
@@ -5214,6 +5444,18 @@ finalize(GObject *object)
     NML3Cfg *self = NM_L3CFG(object);
     gboolean changed;
 
+    if (self->priv.netns) {
+        nm_netns_watcher_remove_all(self->priv.netns,
+                                    _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET),
+                                    TRUE);
+        nm_netns_watcher_remove_all(self->priv.netns,
+                                    _NETNS_WATCHER_IP_ADDR_TAG(self, AF_INET6),
+                                    TRUE);
+    }
+
+    nm_prioq_destroy(&self->priv.p->failedobj_prioq);
+    nm_clear_g_source_inst(&self->priv.p->failedobj_timeout_source);
+
     nm_assert(c_list_is_empty(&self->internal_netns.signal_pending_lst));
     nm_assert(c_list_is_empty(&self->internal_netns.ecmp_track_ifindex_lst_head));
 
@@ -5241,11 +5483,8 @@ finalize(GObject *object)
     nm_clear_g_source_inst(&self->priv.p->nacd_instance_ensure_retry);
     nm_clear_g_source_inst(&self->priv.p->nacd_event_down_source);
 
-    nm_clear_g_source_inst(&self->priv.p->obj_state_temporary_not_available_timeout_source);
-
     nm_clear_pointer(&self->priv.p->obj_state_hash, g_hash_table_destroy);
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_lst_head));
-    nm_assert(c_list_is_empty(&self->priv.p->obj_state_temporary_not_available_lst_head));
     nm_assert(c_list_is_empty(&self->priv.p->obj_state_zombie_lst_head));
 
     if (_nodev_routes_untrack(self, AF_INET))
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index 9d622b4a..5ee201e7 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -55,15 +55,11 @@ typedef enum _nm_packed {
  *   "don't change" behavior. At least once. If the address/route
  *   is still not (no longer) configured on the subsequent
  *   commit, it's not getting added again.
- * @NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE: if set, objects in the
- *   NML3ConfigData are committed to platform even if they were
- *   removed externally.
  */
 typedef enum _nm_packed {
     NM_L3CFG_CONFIG_FLAGS_NONE               = 0,
     NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD       = (1LL << 0),
     NM_L3CFG_CONFIG_FLAGS_ASSUME_CONFIG_ONCE = (1LL << 1),
-    NM_L3CFG_CONFIG_FLAGS_FORCE_ONCE         = (1LL << 2),
 } NML3CfgConfigFlags;
 
 typedef enum _nm_packed {
@@ -132,8 +128,6 @@ typedef enum {
      * and neither should you call into NML3Cfg again (reentrancy). */
     NM_L3_CONFIG_NOTIFY_TYPE_L3CD_CHANGED,
 
-    NM_L3_CONFIG_NOTIFY_TYPE_ROUTES_TEMPORARY_NOT_AVAILABLE_EXPIRED,
-
     NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT,
 
     /* emitted before the merged l3cd is committed to platform.
@@ -412,7 +406,7 @@ gboolean nm_l3cfg_check_ready(NML3Cfg               *self,
                               NML3CfgCheckReadyFlags flags,
                               GArray               **conflicts);
 
-gboolean nm_l3cfg_has_temp_not_available_obj(NML3Cfg *self, int addr_family);
+gboolean nm_l3cfg_has_failedobj_pending(NML3Cfg *self, int addr_family);
 
 /*****************************************************************************/
 
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 19ca1d1e..9c721220 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -69,6 +69,36 @@ typedef struct {
     bool os_owner : 1;
 } RfkillRadioState;
 
+#define AUTOCONNECT_RESET_RETRIES_TIMER_SEC 300
+
+typedef struct {
+    NMDevice             *device;
+    NMSettingsConnection *sett_conn;
+    CList                 dev_lst;
+    CList                 con_lst;
+
+    /* Autoconnet retries needs to be tracked for each (device, connection)
+     * tuple because when a connection is a multiconnect one, each valid device
+     * must try to autoconnect the retries defined in the connection. */
+    struct {
+        guint32                            retries;
+        gint32                             blocked_until_sec;
+        NMSettingsAutoconnectBlockedReason blocked_reason;
+        bool                               initialized : 1;
+    } autoconnect;
+
+} DevConData;
+
+#define DEV_CON_DATA_LOG_FMT \
+    "device[" NM_HASH_OBFUSCATE_PTR_FMT ",%s]-profile[" NM_HASH_OBFUSCATE_PTR_FMT ",%s]"
+
+/* This is an unsafe macro (it evaluates the macro arguments multiple times and is non-function-like). */
+#define DEV_CON_DATA_LOG_ARGS(device, sett_conn)                                                  \
+    NM_HASH_OBFUSCATE_PTR(device), nm_device_get_iface(device), NM_HASH_OBFUSCATE_PTR(sett_conn), \
+        nm_settings_connection_get_id(sett_conn)
+
+#define DEV_CON_DATA_LOG_ARGS_DATA(data) DEV_CON_DATA_LOG_ARGS((data)->device, (data)->sett_conn)
+
 typedef enum {
     ASYNC_OP_TYPE_AC_AUTH_ACTIVATE_INTERNAL,
     ASYNC_OP_TYPE_AC_AUTH_ACTIVATE_USER,
@@ -173,6 +203,8 @@ typedef struct {
     } prop_filter;
     NMRfkillManager *rfkill_mgr;
 
+    GHashTable *devcon_data_dict;
+
     CList link_cb_lst;
 
     NMCheckpointManager *checkpoint_mgr;
@@ -415,6 +447,12 @@ static void _activation_auth_done(NMManager             *self,
 
 static void _rfkill_update(NMManager *self, NMRfkillType rtype);
 
+static DevConData *_devcon_lookup_data(NMManager            *self,
+                                       NMDevice             *device,
+                                       NMSettingsConnection *sett_conn,
+                                       gboolean              create,
+                                       gboolean              log_creation);
+
 /*****************************************************************************/
 
 static NM_CACHED_QUARK_FCN("autoconnect-root", autoconnect_root_quark);
@@ -1212,6 +1250,473 @@ active_connection_get_by_path(NMManager *self, const char *path)
 
 /*****************************************************************************/
 
+static guint32
+_autoconnect_retries_initial(NMSettingsConnection *sett_conn)
+{
+    NMSettingConnection *s_con;
+    int                  retries = -1;
+
+    s_con = nm_connection_get_setting_connection(nm_settings_connection_get_connection(sett_conn));
+    if (s_con)
+        retries = nm_setting_connection_get_autoconnect_retries(s_con);
+
+    if (retries == -1)
+        retries = nm_config_data_get_autoconnect_retries_default(NM_CONFIG_GET_DATA);
+
+    nm_assert(retries >= 0 && retries <= G_MAXINT32);
+
+    if (retries == 0)
+        return NM_AUTOCONNECT_RETRIES_FOREVER;
+    return (guint32) retries;
+}
+
+static gboolean
+_autoconnect_retries_set(NMManager *self, DevConData *data, guint32 retries, gboolean is_reset)
+{
+    gboolean changed = FALSE;
+    gint32   blocked_until_sec;
+
+    nm_assert(data);
+
+    if (!data->autoconnect.initialized || data->autoconnect.retries != retries) {
+        data->autoconnect.initialized = TRUE;
+        data->autoconnect.retries     = retries;
+        changed                       = TRUE;
+    }
+
+    if (retries != 0) {
+        blocked_until_sec = 0;
+    } else {
+        /* NOTE: the blocked time must be identical for all connections, otherwise
+         * the tracking of resetting the retry count in NMPolicy needs adjustment
+         * in _connection_autoconnect_retries_set() (as it would need to re-evaluate
+         * the next-timeout every time a connection gets blocked). */
+        blocked_until_sec =
+            nm_utils_get_monotonic_timestamp_sec() + AUTOCONNECT_RESET_RETRIES_TIMER_SEC;
+    }
+
+    if (data->autoconnect.blocked_until_sec != blocked_until_sec) {
+        data->autoconnect.blocked_until_sec = blocked_until_sec;
+        changed                             = TRUE;
+    }
+
+    if (changed) {
+        char sbuf[200];
+
+        _LOGT(LOGD_SETTINGS,
+              "block-autoconnect: " DEV_CON_DATA_LOG_FMT ": retries set %u%s%s",
+              DEV_CON_DATA_LOG_ARGS_DATA(data),
+              retries,
+              is_reset ? " (is-reset)" : "",
+              blocked_until_sec == 0 ? ""
+                                     : nm_sprintf_buf(sbuf,
+                                                      " (blocked for %d sec)",
+                                                      AUTOCONNECT_RESET_RETRIES_TIMER_SEC));
+    }
+
+    return changed;
+}
+
+/**
+ * nm_manager_devcon_autoconnect_retries_get:
+ * @self: the #NMManager
+ * @device: the #NMDevice
+ * @sett_conn: the #NMSettingsConnection
+ *
+ * Returns the number of autoconnect retries left for the (device, connection)
+ * tuple. If the value is not yet set, initialize it with the value from the
+ * connection or with the global default.
+ */
+guint32
+nm_manager_devcon_autoconnect_retries_get(NMManager            *self,
+                                          NMDevice             *device,
+                                          NMSettingsConnection *sett_conn)
+{
+    DevConData *data;
+
+    nm_assert(NM_IS_MANAGER(self));
+    nm_assert(NM_IS_DEVICE(device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+    nm_assert(self == nm_device_get_manager(device));
+    nm_assert(self == nm_settings_connection_get_manager(sett_conn));
+
+    data = _devcon_lookup_data(self, device, sett_conn, TRUE, FALSE);
+
+    if (G_UNLIKELY(!data->autoconnect.initialized))
+        _autoconnect_retries_set(self, data, _autoconnect_retries_initial(sett_conn), FALSE);
+
+    return data->autoconnect.retries;
+}
+
+void
+nm_manager_devcon_autoconnect_retries_set(NMManager            *self,
+                                          NMDevice             *device,
+                                          NMSettingsConnection *sett_conn,
+                                          guint32               retries)
+{
+    _autoconnect_retries_set(self,
+                             _devcon_lookup_data(self, device, sett_conn, TRUE, FALSE),
+                             retries,
+                             FALSE);
+}
+
+gboolean
+nm_manager_devcon_autoconnect_retries_reset(NMManager            *self,
+                                            NMDevice             *device,
+                                            NMSettingsConnection *sett_conn)
+{
+    DevConData *data;
+    guint32     retries_initial;
+    gboolean    changed = FALSE;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    retries_initial = _autoconnect_retries_initial(sett_conn);
+
+    if (device) {
+        return _autoconnect_retries_set(self,
+                                        _devcon_lookup_data(self, device, sett_conn, TRUE, FALSE),
+                                        retries_initial,
+                                        TRUE);
+    }
+
+    c_list_for_each_entry (data, &sett_conn->devcon_con_lst_head, con_lst) {
+        if (_autoconnect_retries_set(self, data, retries_initial, TRUE))
+            changed = TRUE;
+    }
+
+    return changed;
+}
+
+/**
+ * nm_manager_devcon_autoconnect_reset_reconnect_all:
+ * @self: the #NMManager
+ * @device: the #NMDevice
+ * @sett_conn: the #NMSettingsConnection
+ * @only_no_secrets: boolean to reset all reasons or only no secrets.
+ *
+ * Returns a boolean indicating if something changed or not when resetting the
+ * blocked reasons. If a #NMDevice is present then we also reset the reasons
+ * for the (device, connection) tuple.
+ */
+gboolean
+nm_manager_devcon_autoconnect_reset_reconnect_all(NMManager            *self,
+                                                  NMDevice             *device,
+                                                  NMSettingsConnection *sett_conn,
+                                                  gboolean              only_no_secrets)
+{
+    gboolean changed = FALSE;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    if (only_no_secrets) {
+        /* we only reset the no-secrets blocked flag. */
+        if (nm_settings_connection_autoconnect_blocked_reason_set(
+                sett_conn,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS,
+                FALSE)) {
+            /* maybe the connection is still blocked afterwards for other reasons
+             * and in the larger picture nothing changed. Check if the connection
+             * is still blocked or not. */
+            if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
+                changed = TRUE;
+        }
+
+        return changed;
+    }
+
+    /* we reset the tries-count and any blocked-reason... */
+
+    nm_manager_devcon_autoconnect_retries_reset(self, NULL, sett_conn);
+
+    if (device) {
+        if (nm_manager_devcon_autoconnect_blocked_reason_set(
+                self,
+                device,
+                sett_conn,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
+                FALSE))
+            changed = TRUE;
+    }
+
+    /* we remove all the blocked reason from the connection, if something
+     * happened, then it means the status changed */
+    if (nm_settings_connection_autoconnect_blocked_reason_set(
+            sett_conn,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS
+                | NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST,
+            FALSE))
+        changed = TRUE;
+
+    return changed;
+}
+
+gint32
+nm_manager_devcon_autoconnect_retries_blocked_until(NMManager            *self,
+                                                    NMDevice             *device,
+                                                    NMSettingsConnection *sett_conn)
+{
+    DevConData *data;
+    gint32      min_stamp;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    if (device) {
+        data = _devcon_lookup_data(self, device, sett_conn, FALSE, FALSE);
+
+        if (!data)
+            return 0;
+
+        return data->autoconnect.blocked_until_sec;
+    }
+
+    min_stamp = 0;
+    c_list_for_each_entry (data, &sett_conn->devcon_con_lst_head, con_lst) {
+        gint32 condev_stamp = data->autoconnect.blocked_until_sec;
+
+        if (condev_stamp == 0)
+            continue;
+
+        if (min_stamp == 0 || min_stamp > condev_stamp)
+            min_stamp = condev_stamp;
+    }
+
+    return min_stamp;
+}
+
+gboolean
+nm_manager_devcon_autoconnect_is_blocked(NMManager            *self,
+                                         NMDevice             *device,
+                                         NMSettingsConnection *sett_conn)
+{
+    DevConData *data;
+
+    nm_assert(NM_IS_DEVICE(device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    if (nm_settings_connection_autoconnect_is_blocked(sett_conn))
+        return TRUE;
+
+    data = _devcon_lookup_data(self, device, sett_conn, FALSE, FALSE);
+
+    if (!data)
+        return FALSE;
+
+    if (data->autoconnect.blocked_reason != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE)
+        return TRUE;
+
+    if (data->autoconnect.initialized && data->autoconnect.retries == 0)
+        return TRUE;
+
+    return FALSE;
+}
+
+gboolean
+nm_manager_devcon_autoconnect_blocked_reason_set(NMManager                         *self,
+                                                 NMDevice                          *device,
+                                                 NMSettingsConnection              *sett_conn,
+                                                 NMSettingsAutoconnectBlockedReason value,
+                                                 gboolean                           set)
+{
+    NMSettingsAutoconnectBlockedReason v;
+    DevConData                        *data;
+    gboolean                           changed = FALSE;
+    char                               buf[100];
+
+    nm_assert(!sett_conn || NM_IS_SETTINGS_CONNECTION(sett_conn));
+    nm_assert(!device || NM_IS_DEVICE(device));
+    nm_assert(value != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE);
+    nm_assert(!NM_FLAGS_ANY(value, ~(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED)));
+
+    if (!sett_conn) {
+        if (!device)
+            g_return_val_if_reached(FALSE);
+        c_list_for_each_entry (data, &device->devcon_dev_lst_head, dev_lst) {
+            v = data->autoconnect.blocked_reason;
+            v = NM_FLAGS_ASSIGN(v, value, set);
+
+            if (data->autoconnect.blocked_reason == v)
+                continue;
+
+            _LOGT(LOGD_SETTINGS,
+                  "block-autoconnect: " DEV_CON_DATA_LOG_FMT ": set blocked reason %s",
+                  DEV_CON_DATA_LOG_ARGS_DATA(data),
+                  nm_settings_autoconnect_blocked_reason_to_string(v, buf, sizeof(buf)));
+            data->autoconnect.blocked_reason = v;
+            changed                          = TRUE;
+        }
+        return changed;
+    }
+
+    if (device) {
+        data = _devcon_lookup_data(self, device, sett_conn, TRUE, TRUE);
+        v    = data->autoconnect.blocked_reason;
+        v    = NM_FLAGS_ASSIGN(v, value, set);
+
+        if (data->autoconnect.blocked_reason == v)
+            return FALSE;
+
+        data->autoconnect.blocked_reason = v;
+        _LOGT(LOGD_SETTINGS,
+              "block-autoconnect: " DEV_CON_DATA_LOG_FMT ": set blocked reason %s",
+              DEV_CON_DATA_LOG_ARGS_DATA(data),
+              nm_settings_autoconnect_blocked_reason_to_string(v, buf, sizeof(buf)));
+        return TRUE;
+    }
+
+    c_list_for_each_entry (data, &sett_conn->devcon_con_lst_head, con_lst) {
+        v = data->autoconnect.blocked_reason;
+        v = NM_FLAGS_ASSIGN(v, value, set);
+
+        if (data->autoconnect.blocked_reason == v)
+            continue;
+
+        _LOGT(LOGD_SETTINGS,
+              "block-autoconnect: " DEV_CON_DATA_LOG_FMT ": set blocked reason %s",
+              DEV_CON_DATA_LOG_ARGS_DATA(data),
+              nm_settings_autoconnect_blocked_reason_to_string(v, buf, sizeof(buf)));
+        data->autoconnect.blocked_reason = v;
+        changed                          = TRUE;
+    }
+
+    return changed;
+}
+
+/*****************************************************************************/
+
+static guint
+_devcon_data_hash(gconstpointer ptr)
+{
+    const DevConData *data = ptr;
+
+    nm_assert(NM_IS_DEVICE(data->device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(data->sett_conn));
+
+    return nm_hash_vals(1832112199u, data->device, data->sett_conn);
+}
+
+static gboolean
+_devcon_data_equal(gconstpointer ptr_a, gconstpointer ptr_b)
+{
+    const DevConData *data_a = ptr_a;
+    const DevConData *data_b = ptr_b;
+
+    nm_assert(NM_IS_DEVICE(data_a->device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(data_a->sett_conn));
+    nm_assert(NM_IS_DEVICE(data_b->device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(data_b->sett_conn));
+
+    return data_a->device == data_b->device && data_a->sett_conn == data_b->sett_conn;
+}
+
+static DevConData *
+_devcon_lookup_data(NMManager            *self,
+                    NMDevice             *device,
+                    NMSettingsConnection *sett_conn,
+                    gboolean              create,
+                    gboolean              log_creation)
+{
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+    DevConData       *data;
+    DevConData        needle;
+
+    nm_assert(NM_IS_DEVICE(device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+    nm_assert(self == nm_device_get_manager(device));
+    nm_assert(self == nm_settings_connection_get_manager(sett_conn));
+
+    needle.device    = device;
+    needle.sett_conn = sett_conn;
+
+    data = g_hash_table_lookup(priv->devcon_data_dict, &needle);
+
+    if (data)
+        return data;
+    if (!create)
+        return NULL;
+
+    data  = g_slice_new(DevConData);
+    *data = (DevConData){
+        .device    = device,
+        .sett_conn = sett_conn,
+        .autoconnect =
+            {
+                .initialized       = FALSE,
+                .retries           = 0,
+                .blocked_until_sec = 0,
+                .blocked_reason    = NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE,
+            },
+    };
+    c_list_link_tail(&device->devcon_dev_lst_head, &data->dev_lst);
+    c_list_link_tail(&sett_conn->devcon_con_lst_head, &data->con_lst);
+
+    g_hash_table_add(priv->devcon_data_dict, data);
+
+    if (log_creation) {
+        _LOGT(LOGD_SETTINGS,
+              "block-autoconnect: " DEV_CON_DATA_LOG_FMT ": entry created (not initialized)",
+              DEV_CON_DATA_LOG_ARGS_DATA(data));
+    }
+
+    return data;
+}
+
+static void
+_devcon_remove_data(NMManager *self, DevConData *data)
+{
+    NMManagerPrivate *priv = NM_MANAGER_GET_PRIVATE(self);
+
+    nm_assert(data);
+    nm_assert(NM_IS_DEVICE(data->device));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(data->sett_conn));
+    nm_assert(data == _devcon_lookup_data(self, data->device, data->sett_conn, FALSE, FALSE));
+
+    c_list_unlink_stale(&data->dev_lst);
+    c_list_unlink_stale(&data->con_lst);
+    g_hash_table_remove(priv->devcon_data_dict, data);
+    nm_g_slice_free(data);
+}
+
+static gboolean
+_devcon_remove_device_all(NMManager *self, NMDevice *device)
+{
+    DevConData *data;
+    gboolean    changed;
+
+    nm_assert(NM_IS_DEVICE(device));
+
+    while ((data = c_list_first_entry(&device->devcon_dev_lst_head, DevConData, dev_lst))) {
+        changed = TRUE;
+        _devcon_remove_data(self, data);
+    }
+
+    return changed;
+}
+
+static gboolean
+_devcon_remove_sett_conn_all(NMManager *self, NMSettingsConnection *sett_conn)
+{
+    DevConData *data;
+    gboolean    changed = FALSE;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    while ((data = c_list_first_entry(&sett_conn->devcon_con_lst_head, DevConData, con_lst))) {
+        changed = TRUE;
+        _devcon_remove_data(self, data);
+    }
+
+    return changed;
+}
+
+void
+nm_manager_notify_delete_settings_connections(NMManager *self, NMSettingsConnection *sett_conn)
+{
+    _devcon_remove_sett_conn_all(self, sett_conn);
+}
+
+/*****************************************************************************/
+
 static void
 _config_changed_cb(NMConfig           *config,
                    NMConfigData       *config_data,
@@ -1429,7 +1934,7 @@ find_device_by_iface(NMManager    *self,
     c_list_for_each_entry (candidate, &priv->devices_lst_head, devices_lst) {
         if (!nm_streq(nm_device_get_iface(candidate), iface))
             continue;
-        if (connection && !nm_device_check_connection_compatible(candidate, connection, NULL))
+        if (connection && !nm_device_check_connection_compatible(candidate, connection, TRUE, NULL))
             continue;
         if (slave) {
             if (!nm_device_is_master(candidate))
@@ -1804,7 +2309,7 @@ remove_device(NMManager *self, NMDevice *device, gboolean quitting)
                 nm_device_sys_iface_state_set(device, NM_DEVICE_SYS_IFACE_STATE_REMOVED);
                 nm_device_set_unmanaged_by_flags(device,
                                                  NM_UNMANAGED_PLATFORM_INIT,
-                                                 TRUE,
+                                                 NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                  NM_DEVICE_STATE_REASON_REMOVED);
             }
         }
@@ -1814,6 +2319,8 @@ remove_device(NMManager *self, NMDevice *device, gboolean quitting)
 
     nm_settings_device_removed(priv->settings, device, quitting);
 
+    _devcon_remove_device_all(self, device);
+
     c_list_unlink(&device->devices_lst);
 
     _parent_notify_changed(self, device, TRUE);
@@ -1931,6 +2438,7 @@ find_parent_device_for_connection(NMManager       *self,
             && nm_device_check_connection_compatible(
                 candidate,
                 nm_settings_connection_get_connection(parent_connection),
+                TRUE,
                 NULL))
             first_compatible = candidate;
     }
@@ -2088,9 +2596,10 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
     guint                        i;
     gs_free char                *iface = NULL;
     const char                  *parent_spec;
-    NMDevice                    *device = NULL, *parent = NULL;
+    NMDevice                    *device = NULL;
+    NMDevice                    *parent = NULL;
     NMDevice                    *dev_candidate;
-    GError                      *error = NULL;
+    gs_free_error GError        *error = NULL;
     NMLogLevel                   log_level;
 
     g_return_val_if_fail(NM_IS_MANAGER(self), NULL);
@@ -2099,7 +2608,6 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
     iface = nm_manager_get_connection_iface(self, connection, &parent, &parent_spec, &error);
     if (!iface) {
         _LOG3D(LOGD_DEVICE, connection, "can't get a name of a virtual device: %s", error->message);
-        g_error_free(error);
         return NULL;
     }
 
@@ -2110,7 +2618,7 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
 
     /* See if there's a device that is already compatible with this connection */
     c_list_for_each_entry (dev_candidate, &priv->devices_lst_head, devices_lst) {
-        if (nm_device_check_connection_compatible(dev_candidate, connection, NULL)) {
+        if (nm_device_check_connection_compatible(dev_candidate, connection, FALSE, NULL)) {
             if (nm_device_is_real(dev_candidate)) {
                 _LOG3D(LOGD_DEVICE, connection, "already created virtual interface name %s", iface);
                 return NULL;
@@ -2137,7 +2645,6 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
         device = nm_device_factory_create_device(factory, iface, NULL, connection, NULL, &error);
         if (!device) {
             _LOG3W(LOGD_DEVICE, connection, "factory can't create the device: %s", error->message);
-            g_error_free(error);
             return NULL;
         }
 
@@ -2148,7 +2655,6 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
                    connection,
                    "can't register the device with manager: %s",
                    error->message);
-            g_error_free(error);
             g_object_unref(device);
             return NULL;
         }
@@ -2169,7 +2675,6 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
 
     if (!find_master(self, connection, device, NULL, NULL, NULL, &error)) {
         _LOG3D(LOGD_DEVICE, connection, "skip activation: %s", error->message);
-        g_error_free(error);
         return device;
     }
 
@@ -2179,11 +2684,10 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
         NMConnection        *candidate = nm_settings_connection_get_connection(connections[i]);
         NMSettingConnection *s_con;
 
-        if (!nm_device_check_connection_compatible(device, candidate, NULL))
+        if (!nm_device_check_connection_compatible(device, candidate, TRUE, NULL))
             continue;
 
         s_con = nm_connection_get_setting_connection(candidate);
-        g_assert(s_con);
         if (!nm_setting_connection_get_autoconnect(s_con)
             || nm_settings_connection_autoconnect_is_blocked(connections[i]))
             continue;
@@ -2199,7 +2703,6 @@ system_create_virtual_device(NMManager *self, NMConnection *connection)
                     connection,
                     "couldn't create the device: %s",
                     error->message);
-            g_error_free(error);
             return NULL;
         }
 
@@ -2666,6 +3169,16 @@ _rfkill_update_from_user(NMManager *self, NMRfkillType rtype, gboolean enabled)
 
 /*****************************************************************************/
 
+void
+nm_manager_device_recheck_auto_activate_schedule(NMManager *self, NMDevice *device)
+{
+    g_return_if_fail(NM_IS_MANAGER(self));
+
+    nm_policy_device_recheck_auto_activate_schedule(NM_MANAGER_GET_PRIVATE(self)->policy, device);
+}
+
+/*****************************************************************************/
+
 static void
 device_auth_done_cb(NMAuthChain *chain, GDBusMethodInvocation *context, gpointer user_data)
 {
@@ -2821,7 +3334,7 @@ new_activation_allowed_for_connection(NMManager *self, NMSettingsConnection *con
  * get_existing_connection:
  * @manager: #NMManager instance
  * @device: #NMDevice instance
- * @out_generated: (allow-none): return TRUE, if the connection was generated.
+ * @out_generated: (out) (optional): return TRUE, if the connection was generated.
  *
  * Returns: a #NMSettingsConnection to be assumed by the device, or %NULL if
  *   the device does not support assuming existing connections.
@@ -2882,11 +3395,12 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
         }
     }
 
-    if (nm_config_data_get_device_config_boolean(NM_CONFIG_GET_DATA,
-                                                 NM_CONFIG_KEYFILE_KEY_DEVICE_KEEP_CONFIGURATION,
-                                                 device,
-                                                 TRUE,
-                                                 TRUE)) {
+    if (nm_config_data_get_device_config_boolean_by_device(
+            NM_CONFIG_GET_DATA,
+            NM_CONFIG_KEYFILE_KEY_DEVICE_KEEP_CONFIGURATION,
+            device,
+            TRUE,
+            TRUE)) {
         /* The core of the API is nm_device_generate_connection() function, based on
          * update_connection() virtual method and the @connection_type_supported
          * class attribute. Devices that support assuming existing connections must
@@ -2930,6 +3444,7 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
         && nm_device_check_connection_compatible(
             device,
             nm_settings_connection_get_connection(connection_checked),
+            TRUE,
             NULL)) {
         if (connection) {
             NMConnection *con = nm_settings_connection_get_connection(connection_checked);
@@ -2968,6 +3483,7 @@ get_existing_connection(NMManager *self, NMDevice *device, gboolean *out_generat
                     && nm_device_check_connection_compatible(
                         device,
                         nm_settings_connection_get_connection(sett_conn),
+                        TRUE,
                         NULL))
                     sett_conns[j++] = sett_conn;
             }
@@ -3448,15 +3964,15 @@ _device_realize_finish(NMManager *self, NMDevice *device, const NMPlatformLink *
      * is still unavailable. Set UNAVAILABLE state again, this time with NOW_MANAGED. */
     nm_device_state_changed(device,
                             NM_DEVICE_STATE_UNAVAILABLE,
-                            NM_DEVICE_STATE_REASON_NOW_MANAGED);
-    nm_device_emit_recheck_auto_activate(device);
+                            nm_device_get_manage_reason_external(device));
+    nm_manager_device_recheck_auto_activate_schedule(self, device);
 }
 
 /**
  * add_device:
  * @self: the #NMManager
  * @device: the #NMDevice to add
- * @error: (out): the #GError
+ * @error: the #GError
  *
  * If successful, this function will increase the references count of @device.
  * Callers should decrease the reference count.
@@ -3813,7 +4329,7 @@ _check_remove_dev_on_link_deleted(NMManager *self, NMDevice *device)
                          NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED))
             continue;
 
-        if (!nm_device_check_connection_compatible(device, con, NULL))
+        if (!nm_device_check_connection_compatible(device, con, TRUE, NULL))
             continue;
 
         /* Found a virtual connection compatible, the device must
@@ -4390,64 +4906,136 @@ find_master(NMManager             *self,
             NMActiveConnection   **out_master_ac,
             GError               **error)
 {
-    NMManagerPrivate     *priv = NM_MANAGER_GET_PRIVATE(self);
-    NMSettingConnection  *s_con;
-    const char           *master;
-    NMDevice             *master_device = NULL;
-    NMSettingsConnection *master_connection;
+    NMManagerPrivate            *priv = NM_MANAGER_GET_PRIVATE(self);
+    NMSettingConnection         *s_con;
+    const char                  *master;
+    NMDevice                    *master_device     = NULL;
+    NMSettingsConnection        *master_connection = NULL;
+    NMSettingsConnection *const *connections;
+    guint                        i;
 
-    s_con = nm_connection_get_setting_connection(connection);
-    g_assert(s_con);
+    nm_assert(!out_master_connection || !*out_master_connection);
+    nm_assert(!out_master_device || !*out_master_device);
+    nm_assert(!out_master_ac || !*out_master_ac);
+
+    s_con  = nm_connection_get_setting_connection(connection);
     master = nm_setting_connection_get_master(s_con);
 
     if (master == NULL)
         return TRUE; /* success, but no master */
 
-    /* Try as an interface name first */
-    master_device = find_device_by_iface(self, master, NULL, connection);
-    if (master_device) {
-        if (master_device == device) {
-            g_set_error_literal(error,
-                                NM_MANAGER_ERROR,
-                                NM_MANAGER_ERROR_DEPENDENCY_FAILED,
-                                "Device cannot be its own master");
-            return FALSE;
+    _LOGD(LOGD_CORE,
+          "Looking for a master '%s' for connection '%s' (%s)",
+          master,
+          nm_connection_get_id(connection),
+          nm_connection_get_uuid(connection));
+
+    connections = nm_settings_get_connections_sorted_by_autoconnect_priority(priv->settings, NULL);
+    for (i = 0; connections[i]; i++) {
+        NMConnection *master_candidate = nm_settings_connection_get_connection(connections[i]);
+        NMDevice     *device_candidate;
+
+        if (nm_streq(nm_connection_get_uuid(master_candidate), master)) {
+            if (!is_compatible_with_slave(master_candidate, connection)) {
+                g_set_error(error,
+                            NM_MANAGER_ERROR,
+                            NM_MANAGER_ERROR_DEPENDENCY_FAILED,
+                            "The active connection on %s is not compatible",
+                            nm_device_get_iface(master_device));
+                return FALSE;
+            }
+
+            _LOGD(LOGD_CORE,
+                  "Will consider using connection '%s' (%s) as a master for '%s' (%s) "
+                  "because UUID matches",
+                  nm_connection_get_id(master_candidate),
+                  nm_connection_get_uuid(master_candidate),
+                  nm_connection_get_id(connection),
+                  nm_connection_get_uuid(connection));
+
+            master_connection = connections[i];
+        } else if (nm_connection_get_interface_name(master_candidate)
+                   && nm_streq(nm_connection_get_interface_name(master_candidate), master)) {
+            if (!is_compatible_with_slave(master_candidate, connection))
+                continue;
+
+            /* This might be good enough unless we find a better one (already active or UUID match) */
+            if (!master_connection) {
+                master_connection = connections[i];
+                _LOGD(LOGD_CORE,
+                      "Will consider using connection '%s' (%s) as a master for '%s' (%s) "
+                      "because device matches",
+                      nm_connection_get_id(master_candidate),
+                      nm_connection_get_uuid(master_candidate),
+                      nm_connection_get_id(connection),
+                      nm_connection_get_uuid(connection));
+            }
+        } else {
+            /* No match. */
+            continue;
         }
 
-        master_connection = nm_device_get_settings_connection(master_device);
-        if (master_connection
-            && !is_compatible_with_slave(nm_settings_connection_get_connection(master_connection),
-                                         connection)) {
+        /* Check if the master connection is activated on some device already */
+        c_list_for_each_entry (device_candidate, &priv->devices_lst_head, devices_lst) {
+            if (device_candidate == device)
+                continue;
+
+            if (nm_device_get_settings_connection(device_candidate) == connections[i]) {
+                master_device     = device_candidate;
+                master_connection = connections[i];
+                break;
+            }
+        }
+
+        if (master_device) {
+            /* Now we got a connection and also a device. Look no further. */
+            _LOGD(LOGD_CORE,
+                  "Will use connection '%s' (%s) as a master for '%s' (%s)",
+                  nm_connection_get_id(master_candidate),
+                  nm_connection_get_uuid(master_candidate),
+                  nm_connection_get_id(connection),
+                  nm_connection_get_uuid(connection));
+
+            break;
+        }
+    }
+
+    if (!master_connection) {
+        master_device = find_device_by_iface(self, master, NULL, connection);
+        if (!master_device) {
             g_set_error(error,
                         NM_MANAGER_ERROR,
                         NM_MANAGER_ERROR_DEPENDENCY_FAILED,
-                        "The active connection on %s is not compatible",
-                        nm_device_get_iface(master_device));
+                        "Connection or device %s not found",
+                        master);
             return FALSE;
         }
-    } else {
-        /* Try master as a connection UUID */
-        master_connection = nm_settings_get_connection_by_uuid(priv->settings, master);
-        if (master_connection) {
-            NMDevice *candidate;
-
-            /* Check if the master connection is activated on some device already */
-            c_list_for_each_entry (candidate, &priv->devices_lst_head, devices_lst) {
-                if (candidate == device)
-                    continue;
 
-                if (nm_device_get_settings_connection(candidate) == master_connection) {
-                    master_device = candidate;
-                    break;
-                }
-            }
+        if (master_device == device) {
+            g_set_error_literal(error,
+                                NM_MANAGER_ERROR,
+                                NM_MANAGER_ERROR_DEPENDENCY_FAILED,
+                                "Device cannot be its own master");
+            return FALSE;
         }
+
+        _LOGD(LOGD_CORE,
+              "Master connection for '%s' (%s) not found, will use device '%s'",
+              nm_connection_get_id(connection),
+              nm_connection_get_uuid(connection),
+              nm_device_get_iface(master_device));
+    }
+
+    if (!master_device && !master_connection) {
+        g_set_error_literal(error,
+                            NM_MANAGER_ERROR,
+                            NM_MANAGER_ERROR_UNKNOWN_DEVICE,
+                            "Master connection not found or invalid");
+        return FALSE;
     }
 
-    if (out_master_connection)
-        *out_master_connection = master_connection;
-    if (out_master_device)
-        *out_master_device = master_device;
+    NM_SET_OUT(out_master_connection, master_connection);
+    NM_SET_OUT(out_master_device, master_device);
     if (out_master_ac && master_connection) {
         *out_master_ac = active_connection_find(self,
                                                 master_connection,
@@ -4457,15 +5045,7 @@ find_master(NMManager             *self,
                                                 NULL);
     }
 
-    if (master_device || master_connection)
-        return TRUE;
-    else {
-        g_set_error_literal(error,
-                            NM_MANAGER_ERROR,
-                            NM_MANAGER_ERROR_UNKNOWN_DEVICE,
-                            "Master connection not found or invalid");
-        return FALSE;
-    }
+    return TRUE;
 }
 
 /**
@@ -4632,7 +5212,9 @@ ensure_master_active_connection(NMManager            *self,
                 continue;
 
             if (nm_device_is_real(candidate)
-                && nm_device_get_state(candidate) != NM_DEVICE_STATE_DISCONNECTED)
+                && !NM_IN_SET(nm_device_get_state(candidate),
+                              NM_DEVICE_STATE_DISCONNECTED,
+                              NM_DEVICE_STATE_DEACTIVATING))
                 continue;
 
             master_ac = nm_manager_activate_connection(
@@ -4710,8 +5292,9 @@ find_slaves(NMManager            *manager,
                                                                    &n_all_connections);
     for (i = 0; i < n_all_connections; i++) {
         NMSettingsConnection *master_connection = NULL;
-        NMDevice             *master_device     = NULL, *slave_device;
-        NMSettingsConnection *candidate         = all_connections[i];
+        NMDevice             *master_device     = NULL;
+        NMDevice             *slave_device;
+        NMSettingsConnection *candidate = all_connections[i];
 
         find_master(manager,
                     nm_settings_connection_get_connection(candidate),
@@ -4934,12 +5517,12 @@ unmanaged_to_disconnected(NMDevice *device)
      * and force the device to be managed. */
     nm_device_set_unmanaged_by_flags(device,
                                      NM_UNMANAGED_PLATFORM_INIT,
-                                     FALSE,
+                                     NM_UNMAN_FLAG_OP_SET_MANAGED,
                                      NM_DEVICE_STATE_REASON_USER_REQUESTED);
 
     nm_device_set_unmanaged_by_flags(device,
                                      NM_UNMANAGED_USER_EXPLICIT,
-                                     FALSE,
+                                     NM_UNMAN_FLAG_OP_SET_MANAGED,
                                      NM_DEVICE_STATE_REASON_USER_REQUESTED);
 
     if (!nm_device_get_managed(device, FALSE)) {
@@ -5042,9 +5625,66 @@ active_connection_parent_active(NMActiveConnection *active,
 }
 
 static gboolean
+_check_autoconnect_port(NMActiveConnection   *active,
+                        NMSettingsConnection *master_connection,
+                        NMDevice             *master_device,
+                        NMActiveConnection   *master_ac)
+{
+    NMSettingConnection *s_con;
+    NMDevice            *device;
+
+    if (nm_active_connection_get_activation_reason(active) != NM_ACTIVATION_REASON_AUTOCONNECT) {
+        /* This is an explicit activation. Proceed. */
+        return TRUE;
+    }
+
+    if (!master_connection) {
+        /* This is not a port. Proceed. */
+        return TRUE;
+    }
+
+    device = nm_active_connection_get_device(active);
+
+    if (!nm_device_is_real(device)) {
+        /* The device is not real. We don't know about the carrier. Proceed. */
+        return TRUE;
+    }
+
+    if (nm_device_get_ifindex(device) <= 0) {
+        /* The device has no ifindex. It has no concept of carrier. Proceed. */
+        return TRUE;
+    }
+
+    if (nm_device_has_carrier(device)) {
+        /* The device has carrier. Proceed. */
+        return TRUE;
+    }
+
+    s_con = nm_settings_connection_get_setting(master_connection, NM_META_SETTING_TYPE_CONNECTION);
+
+    if (nm_setting_connection_get_autoconnect(s_con)) {
+        /* The controller profile has autoconnect enabled. Here we want to honor
+         * "ignore-carrier=no", which -- as configuration -- only makes sense for
+         * controllers that have autoconnect disable. Proceed. */
+        return TRUE;
+    }
+
+    if (nm_config_data_get_ignore_carrier_for_port(
+            NM_CONFIG_GET_DATA,
+            nm_setting_connection_get_interface_name(s_con),
+            nm_setting_connection_get_connection_type(s_con))) {
+        /* We ignore carrier on the master (as we would do by default). Proceed. */
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static gboolean
 _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **error)
 {
-    NMDevice                *device, *master_device = NULL;
+    NMDevice                *device;
+    NMDevice                *master_device = NULL;
     NMConnection            *applied;
     NMSettingsConnection    *sett_conn;
     NMSettingsConnection    *master_connection   = NULL;
@@ -5120,6 +5760,37 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
         return FALSE;
     }
 
+    /* FIXME: in _check_autoconnect_port() we decide on whether to abort to
+     * activation based on the device's carrier state (and the controller's
+     * ignore-carrier setting).
+     *
+     * At this stage, we might be activating a VLAN attached to a bond
+     * interface. But the VLAN interface may not be created yet, and not have a
+     * carrier state yet.
+     *
+     * We could fix this, by checking again (or exclusively) before attaching
+     * the port to the controller, whether the conditions from
+     * _check_autoconnect_port() hold.  And if they don't, abort activation at
+     * a later stage.
+     *
+     * The problem is, that we already start activating the controller at this
+     * point. Hence, aborting later is not good. What instead maybe should be
+     * done, is that port profiles don't start activating the controller
+     * before they have layer 2 set up.
+     */
+
+    if (!_check_autoconnect_port(active, master_connection, master_device, master_ac)) {
+        /* Usually, port and controller devices can (auto)connect without carrier. However,
+         * the controller has "ignore-carrier=no" configured. If the port autoconnects,
+         * has no carrier and the controller has ignore-carrier=no, then autoconnect
+         * is going to fail. */
+        g_set_error(error,
+                    NM_MANAGER_ERROR,
+                    NM_MANAGER_ERROR_DEPENDENCY_FAILED,
+                    "port has no carrier and controller does not ignore carrier");
+        return FALSE;
+    }
+
     /* Create any backing resources the device needs */
     if (!nm_device_is_real(device)) {
         NMDevice *parent;
@@ -5151,7 +5822,7 @@ _internal_activate_device(NMManager *self, NMActiveConnection *active, GError **
             if (nm_active_connection_get_activation_reason(active)
                     == NM_ACTIVATION_REASON_AUTOCONNECT
                 && NM_FLAGS_HAS(nm_settings_connection_autoconnect_blocked_reason_get(parent_con),
-                                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST)) {
+                                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST)) {
                 g_set_error(error,
                             NM_MANAGER_ERROR,
                             NM_MANAGER_ERROR_DEPENDENCY_FAILED,
@@ -5529,11 +6200,60 @@ fail:
                                         error_desc ?: error->message);
 }
 
+void
+nm_manager_deactivate_ac(NMManager *self, NMSettingsConnection *connection)
+{
+    NMManagerPrivate   *priv = NM_MANAGER_GET_PRIVATE(self);
+    NMActiveConnection *ac;
+    const CList        *tmp_list, *tmp_safe;
+    GError             *error = NULL;
+    AsyncOpData        *async_op_data;
+    AsyncOpData        *async_op_data_safe;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(connection));
+
+    nm_manager_for_each_active_connection_safe (self, ac, tmp_list, tmp_safe) {
+        if (nm_active_connection_get_settings_connection(ac) == connection
+            && (nm_active_connection_get_state(ac) <= NM_ACTIVE_CONNECTION_STATE_ACTIVATED)) {
+            if (!nm_manager_deactivate_connection(self,
+                                                  ac,
+                                                  NM_DEVICE_STATE_REASON_CONNECTION_REMOVED,
+                                                  &error)) {
+                _LOGW(LOGD_DEVICE,
+                      "connection '%s' disappeared, but error deactivating it: (%d) %s",
+                      nm_settings_connection_get_id(connection),
+                      error ? error->code : -1,
+                      error ? error->message : "(unknown)");
+                g_clear_error(&error);
+            }
+        }
+    }
+
+    c_list_for_each_entry_safe (async_op_data,
+                                async_op_data_safe,
+                                &priv->async_op_lst_head,
+                                async_op_lst) {
+        if (!NM_IN_SET(async_op_data->async_op_type,
+                       ASYNC_OP_TYPE_AC_AUTH_ACTIVATE_INTERNAL,
+                       ASYNC_OP_TYPE_AC_AUTH_ACTIVATE_USER,
+                       ASYNC_OP_TYPE_AC_AUTH_ADD_AND_ACTIVATE,
+                       ASYNC_OP_TYPE_AC_AUTH_ADD_AND_ACTIVATE2))
+            continue;
+
+        ac = async_op_data->ac_auth.active;
+        if (nm_active_connection_get_settings_connection(ac) == connection) {
+            nm_active_connection_set_state(ac,
+                                           NM_ACTIVE_CONNECTION_STATE_DEACTIVATED,
+                                           NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED);
+        }
+    }
+}
+
 /**
  * nm_manager_activate_connection():
  * @self: the #NMManager
  * @sett_conn: the #NMSettingsConnection to activate on @device
- * @applied: (allow-none): the applied connection to activate on @device
+ * @applied: (nullable): the applied connection to activate on @device
  * @specific_object: the specific object path, if any, for the activation
  * @device: the #NMDevice to activate @sett_conn on. Can be %NULL for VPNs.
  * @subject: the subject which requested activation
@@ -5782,7 +6502,7 @@ _activation_auth_done(NMManager             *self,
 
     nm_settings_connection_autoconnect_blocked_reason_set(
         connection,
-        NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST,
+        NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST,
         FALSE);
     g_dbus_method_invocation_return_value(
         invocation,
@@ -6492,7 +7212,7 @@ device_sleep_cb(NMDevice *device, GParamSpec *pspec, NMManager *self)
         _LOGD(LOGD_SUSPEND, "sleep: unmanaging device %s", nm_device_get_ip_iface(device));
         nm_device_set_unmanaged_by_flags_queue(device,
                                                NM_UNMANAGED_SLEEPING,
-                                               TRUE,
+                                               NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                NM_DEVICE_STATE_REASON_SLEEPING);
         break;
     case NM_DEVICE_STATE_UNMANAGED:
@@ -6541,6 +7261,8 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                 continue;
             }
 
+            nm_device_notify_sleeping(device);
+
             if (nm_device_is_activating(device)
                 || nm_device_get_state(device) == NM_DEVICE_STATE_ACTIVATED) {
                 _LOGD(LOGD_SUSPEND,
@@ -6554,7 +7276,7 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
             } else {
                 nm_device_set_unmanaged_by_flags(device,
                                                  NM_UNMANAGED_SLEEPING,
-                                                 TRUE,
+                                                 NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                  NM_DEVICE_STATE_REASON_SLEEPING);
             }
         }
@@ -6574,7 +7296,7 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                 if (device_is_wake_on_lan(priv->platform, device))
                     nm_device_set_unmanaged_by_flags(device,
                                                      NM_UNMANAGED_SLEEPING,
-                                                     TRUE,
+                                                     NM_UNMAN_FLAG_OP_SET_UNMANAGED,
                                                      NM_DEVICE_STATE_REASON_SLEEPING);
 
                 /* Check if the device is unmanaged but the state transition is still pending.
@@ -6597,7 +7319,8 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
 
         /* Re-manage managed devices */
         c_list_for_each_entry (device, &priv->devices_lst_head, devices_lst) {
-            guint i;
+            NMDeviceStateReason reason;
+            guint               i;
 
             if (nm_device_is_software(device)
                 && !nm_device_get_unmanaged_flags(device, NM_UNMANAGED_SLEEPING)) {
@@ -6628,10 +7351,17 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                     nm_device_set_enabled(device, enabled);
             }
 
+            /* The reason determines whether the device will be sys-iface-state=managed
+             * or sys-iface-state=external. Pass the correct reason to restore the state
+             * that was set before sleeping. */
+            reason = nm_device_get_sys_iface_state_before_sleep(device)
+                             == NM_DEVICE_SYS_IFACE_STATE_EXTERNAL
+                         ? NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED
+                         : NM_DEVICE_STATE_REASON_NOW_MANAGED;
             nm_device_set_unmanaged_by_flags(device,
                                              NM_UNMANAGED_SLEEPING,
-                                             FALSE,
-                                             NM_DEVICE_STATE_REASON_NOW_MANAGED);
+                                             NM_UNMAN_FLAG_OP_SET_MANAGED,
+                                             reason);
         }
 
         /* Give the connections a chance to recreate the virtual devices.
@@ -7963,6 +8693,14 @@ nm_settings_get(void)
     return NM_MANAGER_GET_PRIVATE(singleton_instance)->settings;
 }
 
+NMPolicy *
+nm_manager_get_policy(NMManager *self)
+{
+    g_return_val_if_fail(NM_IS_MANAGER(self), NULL);
+
+    return NM_MANAGER_GET_PRIVATE(self)->policy;
+}
+
 NMManager *
 nm_manager_setup(void)
 {
@@ -8106,6 +8844,8 @@ nm_manager_init(NMManager *self)
     priv->state    = NM_STATE_DISCONNECTED;
     priv->startup  = TRUE;
 
+    priv->devcon_data_dict = g_hash_table_new(_devcon_data_hash, _devcon_data_equal);
+
     /* sleep/wake handling */
     priv->sleep_monitor = nm_sleep_monitor_new();
     g_signal_connect(priv->sleep_monitor, NM_SLEEP_MONITOR_SLEEPING, G_CALLBACK(sleeping_cb), self);
@@ -8439,6 +9179,8 @@ dispose(GObject *object)
 
     nm_clear_pointer(&priv->device_route_metrics, g_hash_table_destroy);
 
+    nm_clear_pointer(&priv->devcon_data_dict, g_hash_table_destroy);
+
     G_OBJECT_CLASS(nm_manager_parent_class)->dispose(object);
 }
 
diff --git a/src/core/nm-manager.h b/src/core/nm-manager.h
index 0cbbcbf0..3028eb7e 100644
--- a/src/core/nm-manager.h
+++ b/src/core/nm-manager.h
@@ -69,6 +69,8 @@ NMManager *nm_manager_setup(void);
 NMManager *nm_manager_get(void);
 #define NM_MANAGER_GET (nm_manager_get())
 
+NMPolicy *nm_manager_get_policy(NMManager *self);
+
 gboolean nm_manager_start(NMManager *manager, GError **error);
 void     nm_manager_stop(NMManager *manager);
 NMState  nm_manager_get_state(NMManager *manager);
@@ -121,6 +123,10 @@ NMSettingsConnection **nm_manager_get_activatable_connections(NMManager *manager
                                                               gboolean   sort,
                                                               guint     *out_len);
 
+void nm_manager_deactivate_ac(NMManager *self, NMSettingsConnection *connection);
+
+void nm_manager_device_recheck_auto_activate_schedule(NMManager *self, NMDevice *device);
+
 void     nm_manager_write_device_state_all(NMManager *manager);
 gboolean nm_manager_write_device_state(NMManager *manager, NMDevice *device, int *out_ifindex);
 
@@ -207,6 +213,11 @@ struct _NMDnsManager *nm_manager_get_dns_manager(NMManager *self);
 
 /*****************************************************************************/
 
+void nm_manager_notify_delete_settings_connections(NMManager            *self,
+                                                   NMSettingsConnection *sett_conn);
+
+/*****************************************************************************/
+
 void nm_manager_device_auth_request(NMManager                     *self,
                                     NMDevice                      *device,
                                     GDBusMethodInvocation         *context,
@@ -219,4 +230,40 @@ void nm_manager_device_auth_request(NMManager                     *self,
 
 void nm_manager_unblock_failed_ovs_interfaces(NMManager *self);
 
+/*****************************************************************************/
+
+#define NM_AUTOCONNECT_RETRIES_FOREVER G_MAXUINT32
+
+guint32 nm_manager_devcon_autoconnect_retries_get(NMManager            *self,
+                                                  NMDevice             *device,
+                                                  NMSettingsConnection *sett_conn);
+
+void nm_manager_devcon_autoconnect_retries_set(NMManager            *self,
+                                               NMDevice             *device,
+                                               NMSettingsConnection *sett_conn,
+                                               guint32               retries);
+
+gboolean nm_manager_devcon_autoconnect_retries_reset(NMManager            *self,
+                                                     NMDevice             *device,
+                                                     NMSettingsConnection *sett_conn);
+
+gboolean nm_manager_devcon_autoconnect_reset_reconnect_all(NMManager            *self,
+                                                           NMDevice             *device,
+                                                           NMSettingsConnection *sett_conn,
+                                                           gboolean              only_no_secrets);
+
+gint32 nm_manager_devcon_autoconnect_retries_blocked_until(NMManager            *self,
+                                                           NMDevice             *device,
+                                                           NMSettingsConnection *sett_conn);
+
+gboolean nm_manager_devcon_autoconnect_is_blocked(NMManager            *self,
+                                                  NMDevice             *device,
+                                                  NMSettingsConnection *sett_conn);
+
+gboolean nm_manager_devcon_autoconnect_blocked_reason_set(NMManager            *self,
+                                                          NMDevice             *device,
+                                                          NMSettingsConnection *sett_conn,
+                                                          NMSettingsAutoconnectBlockedReason value,
+                                                          gboolean                           set);
+
 #endif /* __NETWORKMANAGER_MANAGER_H__ */
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index 12ca8508..ad156f99 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -22,6 +22,44 @@
 
 /*****************************************************************************/
 
+typedef struct {
+    gconstpointer tag;
+    CList         watcher_by_tag_lst_head;
+} WatcherByTag;
+
+typedef struct {
+    NMIPAddrTyped addr;
+    CList         watcher_ip_addr_lst_head;
+} WatcherDataIPAddr;
+
+struct _NMNetnsWatcherHandle {
+    NMNetnsWatcherType     watcher_type;
+    NMNetnsWatcherData     watcher_data;
+    gconstpointer          tag;
+    NMNetnsWatcherCallback callback;
+    gpointer               callback_user_data;
+
+    /* This is linked to "WatcherByTag.watcher_by_tag_lst_head" in
+     * "priv->watcher_by_tag_idx". */
+    CList watcher_tag_lst;
+
+    /* The registration data, which depends on the "watcher_type". */
+    union {
+        struct {
+            CList watcher_ip_addr_lst;
+        } ip_addr;
+    } reg_data;
+
+    /* nm_netns_watcher_add() will mark the handle as non-dirty, while
+     * nm_netns_watcher_remove_all() can delete only dirty handles (while
+     * leaving non-dirty handles alive, but marking them as dirty).
+     *
+     * That allows a pattern where you just add the new handles that you want
+     * now, and then call nm_netns_watcher_remove_all() to remove those that
+     * should no longer be present. */
+    bool watcher_dirty : 1;
+};
+
 NM_GOBJECT_PROPERTIES_DEFINE_BASE(PROP_PLATFORM, );
 
 typedef struct {
@@ -33,8 +71,20 @@ typedef struct {
     GHashTable       *shared_ips;
     GHashTable       *ecmp_track_by_obj;
     GHashTable       *ecmp_track_by_ecmpid;
-    CList             l3cfg_signal_pending_lst_head;
-    GSource          *signal_pending_idle_source;
+
+    /* Indexes the watcher handles. */
+    GHashTable *watcher_idx;
+
+    /* An index of WatcherByTag. It allows to lookup watcher handles by tag.
+     * Handles without tag are not indexed. */
+    GHashTable *watcher_by_tag_idx;
+
+    /* Index for WatcherDataIPAddr instances. Allows to lookup all subscribers
+     * by IP address. */
+    GHashTable *watcher_ip_data_idx;
+
+    CList    l3cfg_signal_pending_lst_head;
+    GSource *signal_pending_idle_source;
 } NMNetnsPrivate;
 
 struct _NMNetns {
@@ -87,6 +137,24 @@ NM_DEFINE_SINGLETON_GETTER(NMNetns, nm_netns_get, NM_TYPE_NETNS);
 
 /*****************************************************************************/
 
+static WatcherDataIPAddr             *
+_watcher_ip_data_lookup(NMNetns *self, int addr_family, gconstpointer addr);
+static void _watcher_handle_notify(NMNetns                       *self,
+                                   NMNetnsWatcherHandle          *handle,
+                                   const NMNetnsWatcherEventData *event_data);
+static const char *
+_watcher_handle_to_string(const NMNetnsWatcherHandle *handle, char *buf, gsize buf_size);
+
+/*****************************************************************************/
+
+static gboolean
+NM_NETNS_WATCHER_TYPE_VALID(NMNetnsWatcherType watcher_type)
+{
+    return NM_IN_SET(watcher_type, NM_NETNS_WATCHER_TYPE_IP_ADDR);
+}
+
+/*****************************************************************************/
+
 typedef struct {
     const NMPObject *representative_obj;
     const NMPObject *merged_obj;
@@ -437,7 +505,7 @@ _platform_signal_cb(NMPlatform   *platform,
 
     l3cfg = nm_netns_l3cfg_get(self, ifindex);
     if (!l3cfg)
-        return;
+        goto notify_watcher;
 
     l3cfg->internal_netns.signal_pending_obj_type_flags |= nmp_object_type_to_flags(obj_type);
 
@@ -450,6 +518,55 @@ _platform_signal_cb(NMPlatform   *platform,
     }
 
     _nm_l3cfg_notify_platform_change(l3cfg, change_type, NMP_OBJECT_UP_CAST(platform_object));
+
+notify_watcher:
+    switch (obj_type) {
+    case NMP_OBJECT_TYPE_IP4_ADDRESS:
+    case NMP_OBJECT_TYPE_IP6_ADDRESS:
+    {
+        NMNetnsWatcherHandle *handle;
+        NMNetnsWatcherHandle *handle_safe;
+        WatcherDataIPAddr    *data;
+
+        data =
+            _watcher_ip_data_lookup(self,
+                                    obj_type == NMP_OBJECT_TYPE_IP4_ADDRESS ? AF_INET : AF_INET6,
+                                    ((const NMPlatformIPAddress *) platform_object)->address_ptr);
+
+        if (data) {
+            const NMNetnsWatcherEventData event_data = {
+                .ip_addr =
+                    {
+                        .change_type = change_type,
+                        .obj         = NMP_OBJECT_UP_CAST(platform_object),
+                    },
+            };
+            char sbuf[500];
+
+            c_list_for_each_entry_safe (handle,
+                                        handle_safe,
+                                        &data->watcher_ip_addr_lst_head,
+                                        reg_data.ip_addr.watcher_ip_addr_lst) {
+                _LOGT("netns-watcher: %s %s",
+                      "notify",
+                      _watcher_handle_to_string(handle, sbuf, sizeof(sbuf)));
+
+                /* Note that we dispatch these events directly from the platform event
+                 * and while iterating over "data".
+                 *
+                 * From the callback, it's probably a bad idea to do anything in platform
+                 * that might change anything (emit new signals) or to nm_netns_watcher_remove*()
+                 * any other watcher.
+                 *
+                 * The callee needs to be careful. */
+                _watcher_handle_notify(self, handle, &event_data);
+            }
+        }
+        break;
+    }
+    default:
+        break;
+    }
 }
 
 /*****************************************************************************/
@@ -695,7 +812,7 @@ nm_netns_ip_route_ecmp_commit(NMNetns    *self,
                     /* This route is onlink. We don't need to configure an onlink route
                      * to the gateway, and the route is immediately ready for configuration. */
                     track_obj->is_ready = TRUE;
-                } else if (c_list_length_is(&track_ecmpid->ecmpid_lst_head, 1)) {
+                } else if (c_list_is_empty_or_single(&track_ecmpid->ecmpid_lst_head)) {
                     /* This route has no merge partner and ends up being a
                      * single hop route. It will be returned and configured by
                      * the calling "l3cfg".
@@ -815,7 +932,461 @@ nm_netns_ip_route_ecmp_commit(NMNetns    *self,
         if (changed || is_reapply) {
             _LOGT("ecmp-route: multi-hop %s",
                   nmp_object_to_string(route_obj, NMP_OBJECT_TO_STRING_PUBLIC, sbuf, sizeof(sbuf)));
-            nm_platform_ip_route_add(priv->platform, NMP_NLM_FLAG_APPEND, route_obj);
+            nm_platform_ip_route_add(priv->platform, NMP_NLM_FLAG_APPEND, route_obj, NULL);
+        }
+    }
+}
+
+/*****************************************************************************/
+
+static void
+_watcher_data_set(NMNetnsWatcherData       *dst,
+                  NMNetnsWatcherType        watcher_type,
+                  const NMNetnsWatcherData *src)
+{
+    nm_assert(dst);
+    nm_assert(src);
+
+    switch (watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+        dst->ip_addr = src->ip_addr;
+        return;
+    }
+    nm_assert_not_reached();
+}
+
+static void
+_watcher_data_hash(NMHashState *h, NMNetnsWatcherType watcher_type, const NMNetnsWatcherData *data)
+{
+    nm_assert(h);
+    nm_assert(NM_NETNS_WATCHER_TYPE_VALID(watcher_type));
+    nm_assert(data);
+
+    switch (watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+        nm_ip_addr_typed_hash_update(h, &data->ip_addr.addr);
+        return;
+    }
+    nm_assert_not_reached();
+}
+
+static gboolean
+_watcher_data_equal(NMNetnsWatcherType        watcher_type,
+                    const NMNetnsWatcherData *a,
+                    const NMNetnsWatcherData *b)
+{
+    nm_assert(NM_NETNS_WATCHER_TYPE_VALID(watcher_type));
+    nm_assert(a);
+    nm_assert(b);
+
+    switch (watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+        return nm_ip_addr_typed_equal(&a->ip_addr.addr, &b->ip_addr.addr);
+    }
+    return nm_assert_unreachable_val(FALSE);
+}
+
+static void
+_watcher_by_tag_destroy(WatcherByTag *watcher_by_tag)
+{
+    c_list_unlink_stale(&watcher_by_tag->watcher_by_tag_lst_head);
+    nm_g_slice_free(watcher_by_tag);
+}
+
+static void
+_watcher_handle_init(NMNetnsWatcherHandle     *handle,
+                     NMNetnsWatcherType        watcher_type,
+                     const NMNetnsWatcherData *watcher_data,
+                     gconstpointer             tag)
+{
+    nm_assert(handle);
+    nm_assert(NM_NETNS_WATCHER_TYPE_VALID(watcher_type));
+
+    *handle = (NMNetnsWatcherHandle){
+        .watcher_type    = watcher_type,
+        .tag             = tag,
+        .watcher_tag_lst = C_LIST_INIT(handle->watcher_tag_lst),
+    };
+    _watcher_data_set(&handle->watcher_data, watcher_type, watcher_data);
+}
+
+static guint
+_watcher_handle_hash(gconstpointer data)
+{
+    const NMNetnsWatcherHandle *watcher = data;
+    NMHashState                 h;
+
+    nm_assert(watcher);
+    nm_assert(watcher->tag);
+
+    nm_hash_init(&h, 2696278447u);
+    nm_hash_update_vals(&h, watcher->tag, watcher->watcher_type);
+    _watcher_data_hash(&h, watcher->watcher_type, &watcher->watcher_data);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_watcher_handle_equal(gconstpointer a, gconstpointer b)
+{
+    const NMNetnsWatcherHandle *ha = a;
+    const NMNetnsWatcherHandle *hb = b;
+
+    nm_assert(ha);
+    nm_assert(hb);
+    nm_assert(ha->tag);
+    nm_assert(hb->tag);
+
+    if (ha == hb)
+        return TRUE;
+
+    return (ha->tag == hb->tag) && (ha->watcher_type == hb->watcher_type)
+           && _watcher_data_equal(ha->watcher_type, &ha->watcher_data, &hb->watcher_data);
+}
+
+static const char *
+_watcher_handle_to_string(const NMNetnsWatcherHandle *handle, char *buf, gsize buf_size)
+{
+    const char *buf0 = buf;
+    char        sbuf[NM_INET_ADDRSTRLEN];
+
+    nm_strbuf_append(&buf,
+                     &buf_size,
+                     "h:" NM_HASH_OBFUSCATE_PTR_FMT "[",
+                     NM_HASH_OBFUSCATE_PTR(handle));
+
+    if (handle->tag) {
+        nm_strbuf_append(&buf,
+                         &buf_size,
+                         "tag:" NM_HASH_OBFUSCATE_PTR_FMT ",",
+                         NM_HASH_OBFUSCATE_PTR(handle->tag));
+    }
+
+    switch (handle->watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+        nm_strbuf_append_str(&buf, &buf_size, "ip-addr:");
+        nm_strbuf_append_str(&buf,
+                             &buf_size,
+                             nm_inet_ntop(handle->watcher_data.ip_addr.addr.addr_family,
+                                          &handle->watcher_data.ip_addr.addr.addr,
+                                          sbuf));
+        goto out;
+    }
+    nm_assert_not_reached();
+    nm_strbuf_append_str(&buf, &buf_size, "unknown");
+
+out:
+    nm_strbuf_append_c(&buf, &buf_size, ']');
+    return buf0;
+}
+
+static void
+_watcher_handle_notify(NMNetns                       *self,
+                       NMNetnsWatcherHandle          *handle,
+                       const NMNetnsWatcherEventData *event_data)
+{
+    nm_assert(NM_IS_NETNS(self));
+    nm_assert(handle);
+    nm_assert(handle->callback);
+
+    handle->callback(self,
+                     handle->watcher_type,
+                     &handle->watcher_data,
+                     handle->tag,
+                     event_data,
+                     handle->callback_user_data);
+}
+
+static WatcherDataIPAddr *
+_watcher_ip_data_lookup(NMNetns *self, int addr_family, gconstpointer addr)
+{
+    WatcherDataIPAddr needle;
+
+    needle.addr.addr_family = addr_family;
+    nm_ip_addr_set(addr_family, &needle.addr.addr, addr);
+    return g_hash_table_lookup(NM_NETNS_GET_PRIVATE(self)->watcher_ip_data_idx, &needle);
+}
+
+static WatcherDataIPAddr *
+_watcher_ip_data_lookup_addr(NMNetns *self, const NMIPAddrTyped *addr)
+{
+    return _watcher_ip_data_lookup(self, addr->addr_family, &addr->addr);
+}
+
+static guint
+_watcher_ip_data_hash(gconstpointer _data)
+{
+    const WatcherDataIPAddr *data = _data;
+    NMHashState              h;
+
+    nm_assert(data);
+
+    nm_hash_init(&h, 3152126191u);
+    nm_ip_addr_typed_hash_update(&h, &data->addr);
+    return nm_hash_complete(&h);
+}
+
+static gboolean
+_watcher_ip_data_equal(gconstpointer a, gconstpointer b)
+{
+    const WatcherDataIPAddr *data_a = a;
+    const WatcherDataIPAddr *data_b = b;
+
+    nm_assert(data_a);
+    nm_assert(data_b);
+
+    return nm_ip_addr_typed_equal(&data_a->addr, &data_b->addr);
+}
+
+static NMNetnsWatcherHandle *
+_watcher_lookup_handle(NMNetns                  *self,
+                       NMNetnsWatcherType        watcher_type,
+                       const NMNetnsWatcherData *watcher_data,
+                       gconstpointer             tag)
+{
+    NMNetnsWatcherHandle handle_needle;
+
+    nm_assert(NM_IS_NETNS(self));
+    nm_assert(tag);
+
+    _watcher_handle_init(&handle_needle, watcher_type, watcher_data, tag);
+    return g_hash_table_lookup(NM_NETNS_GET_PRIVATE(self)->watcher_idx, &handle_needle);
+}
+
+static void
+_watcher_register_handle(NMNetns *self, NMNetnsWatcherHandle *handle)
+{
+    NMNetnsPrivate *priv = NM_NETNS_GET_PRIVATE(self);
+
+    switch (handle->watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+    {
+        WatcherDataIPAddr *data;
+
+        data = _watcher_ip_data_lookup_addr(self, &handle->watcher_data.ip_addr.addr);
+        if (!data) {
+            data  = g_slice_new(WatcherDataIPAddr);
+            *data = (WatcherDataIPAddr){
+                .addr                     = handle->watcher_data.ip_addr.addr,
+                .watcher_ip_addr_lst_head = C_LIST_INIT(data->watcher_ip_addr_lst_head),
+            };
+            if (!g_hash_table_add(priv->watcher_ip_data_idx, data))
+                nm_assert_not_reached();
+        }
+
+        c_list_link_tail(&data->watcher_ip_addr_lst_head,
+                         &handle->reg_data.ip_addr.watcher_ip_addr_lst);
+        return;
+    }
+    }
+    nm_assert_not_reached();
+}
+
+static void
+_watcher_unregister_handle(NMNetns *self, NMNetnsWatcherHandle *handle)
+{
+    NMNetnsPrivate *priv = NM_NETNS_GET_PRIVATE(self);
+
+    switch (handle->watcher_type) {
+    case NM_NETNS_WATCHER_TYPE_IP_ADDR:
+    {
+        gboolean is_last;
+
+        nm_assert(({
+            WatcherDataIPAddr *d;
+
+            d = _watcher_ip_data_lookup_addr(self, &handle->watcher_data.ip_addr.addr);
+            d &&c_list_contains(&d->watcher_ip_addr_lst_head,
+                                &handle->reg_data.ip_addr.watcher_ip_addr_lst);
+        }));
+
+        is_last = c_list_is_empty_or_single(&handle->reg_data.ip_addr.watcher_ip_addr_lst);
+
+        c_list_unlink(&handle->reg_data.ip_addr.watcher_ip_addr_lst);
+
+        if (is_last) {
+            WatcherDataIPAddr *data;
+
+            data = _watcher_ip_data_lookup_addr(self, &handle->watcher_data.ip_addr.addr);
+            nm_assert(data);
+            nm_assert(c_list_is_empty(&data->watcher_ip_addr_lst_head));
+
+            if (!g_hash_table_remove(priv->watcher_ip_data_idx, data))
+                nm_assert_not_reached();
+
+            nm_g_slice_free(data);
+        }
+        return;
+    }
+    }
+    nm_assert_not_reached();
+}
+
+void
+nm_netns_watcher_add(NMNetns                  *self,
+                     NMNetnsWatcherType        watcher_type,
+                     const NMNetnsWatcherData *watcher_data,
+                     gconstpointer             tag,
+                     NMNetnsWatcherCallback    callback,
+                     gpointer                  user_data)
+{
+    NMNetnsPrivate       *priv;
+    NMNetnsWatcherHandle *handle;
+    gboolean              is_new = FALSE;
+    char                  sbuf[500];
+
+    g_return_if_fail(NM_IS_NETNS(self));
+    g_return_if_fail(NM_NETNS_WATCHER_TYPE_VALID(watcher_type));
+    g_return_if_fail(callback);
+    g_return_if_fail(tag);
+
+    priv = NM_NETNS_GET_PRIVATE(self);
+
+    handle = _watcher_lookup_handle(self, watcher_type, watcher_data, tag);
+
+    if (!handle) {
+        WatcherByTag *watcher_by_tag;
+
+        if (G_UNLIKELY(g_hash_table_size(priv->watcher_idx) == 0))
+            g_object_ref(self);
+
+        handle = g_slice_new(NMNetnsWatcherHandle);
+        _watcher_handle_init(handle, watcher_type, watcher_data, tag);
+
+        if (!g_hash_table_add(priv->watcher_idx, handle))
+            nm_assert_not_reached();
+
+        watcher_by_tag = g_hash_table_lookup(priv->watcher_by_tag_idx, &tag);
+
+        if (!watcher_by_tag) {
+            watcher_by_tag  = g_slice_new(WatcherByTag);
+            *watcher_by_tag = (WatcherByTag){
+                .tag                     = tag,
+                .watcher_by_tag_lst_head = C_LIST_INIT(watcher_by_tag->watcher_by_tag_lst_head),
+            };
+            g_hash_table_add(priv->watcher_by_tag_idx, watcher_by_tag);
+        }
+
+        c_list_link_tail(&watcher_by_tag->watcher_by_tag_lst_head, &handle->watcher_tag_lst);
+
+        is_new = TRUE;
+    } else {
+        /* Handles are deduplicated/shared. Hence it is error prone (and likely
+         * a bug) to provide different callback/user_data. Such usage is
+         * rejected here.
+         *
+         * This could be made to work, for example by now allowing handles to
+         * be merged or simply requiring the caller to be careful to not get
+         * this wrong. But that is currently not implemented nor needed.
+         */
+        nm_assert(!tag
+                  || (handle->callback == callback && handle->callback_user_data == user_data));
+    }
+
+    if (_LOGT_ENABLED()
+        && (is_new || handle->callback != callback || handle->callback_user_data != user_data)) {
+        _LOGT("netns-watcher: %s %s",
+              is_new ? "register" : "update",
+              _watcher_handle_to_string(handle, sbuf, sizeof(sbuf)));
+    }
+
+    handle->callback           = callback;
+    handle->callback_user_data = user_data;
+    handle->watcher_dirty      = FALSE;
+
+    if (is_new)
+        _watcher_register_handle(self, handle);
+
+    /* We cannot return a handle here, because handles are deduplicated via the priv->watchers_idx dictionary.
+     * The usage pattern is to use nm_netns_watcher_remove_all(), and not remove them one by one.
+     * As nm_netns_watcher_add() can return the same handle more than once, the user
+     * wouldn't know when it's safe to call nm_netns_watcher_remove_handle().
+     *
+     * This could be extended by adding a ref-count to the handles. But that is not
+     * used currently, so it's not possible to remove watcher by their handle. */
+}
+
+static void
+nm_netns_watcher_remove_handle(NMNetns *self, NMNetnsWatcherHandle *handle)
+{
+    NMNetnsPrivate *priv;
+    char            sbuf[500];
+
+    g_return_if_fail(NM_IS_NETNS(self));
+    g_return_if_fail(handle);
+    nm_assert(handle->tag);
+
+    priv = NM_NETNS_GET_PRIVATE(self);
+
+    nm_assert(g_hash_table_lookup(priv->watcher_idx, handle) == handle);
+
+    _LOGT("netns-watcher: %s %s",
+          "unregister",
+          _watcher_handle_to_string(handle, sbuf, sizeof(sbuf)));
+
+    _watcher_unregister_handle(self, handle);
+
+    if (!g_hash_table_remove(priv->watcher_idx, handle))
+        nm_assert_not_reached();
+
+    if (c_list_is_empty_or_single(&handle->watcher_tag_lst)) {
+        if (!g_hash_table_remove(priv->watcher_by_tag_idx, &handle->tag))
+            nm_assert_not_reached();
+    }
+
+    c_list_unlink_stale(&handle->watcher_tag_lst);
+    nm_g_slice_free(handle);
+
+    if (G_UNLIKELY(g_hash_table_size(priv->watcher_idx) == 0))
+        g_object_unref(self);
+}
+
+void
+nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag, gboolean all)
+{
+    NMNetnsPrivate       *priv;
+    WatcherByTag         *watcher_by_tag;
+    NMNetnsWatcherHandle *handle;
+    NMNetnsWatcherHandle *handle_safe;
+
+    g_return_if_fail(NM_IS_NETNS(self));
+
+    /* remove-all only works with handles that have a tag associated.
+     * Since NMNetns can have multiple users that are unknown to each
+     * other, it makes no sense to have a remove-all function which
+     * would remove all of them. */
+    g_return_if_fail(tag);
+
+    priv = NM_NETNS_GET_PRIVATE(self);
+
+    watcher_by_tag = g_hash_table_lookup(priv->watcher_by_tag_idx, &tag);
+    if (!watcher_by_tag)
+        return;
+
+    c_list_for_each_entry_safe (handle,
+                                handle_safe,
+                                &watcher_by_tag->watcher_by_tag_lst_head,
+                                watcher_tag_lst) {
+        gboolean is_last;
+
+        if (!all && !handle->watcher_dirty) {
+            /* Survivors are marked as dirty. This enables a pattern where you
+             * call nm_netns_watcher_add() on the elements you care about
+             * (which clears the dirty flag), and then remove all dirty ones
+             * with nm_netns_watcher_remove_all() (which marks the remaining
+             * handles as dirty for the next time). */
+            handle->watcher_dirty = TRUE;
+            continue;
+        }
+
+        is_last = c_list_is_empty_or_single(&watcher_by_tag->watcher_by_tag_lst_head);
+        nm_netns_watcher_remove_handle(self, handle);
+
+        if (is_last) {
+            /* Removing the last handle destroys the "watcher_by_tag" and may even
+             * destroy "self". We must not touch those pointers hereafter.
+             *
+             * If you ever *not* return here, make sure to handle that! */
+            return;
         }
     }
 }
@@ -850,6 +1421,7 @@ nm_netns_init(NMNetns *self)
     NMNetnsPrivate *priv = NM_NETNS_GET_PRIVATE(self);
 
     priv->_self_signal_user_data = self;
+
     c_list_init(&priv->l3cfg_signal_pending_lst_head);
 
     G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(EcmpTrackObj, obj) == 0);
@@ -859,6 +1431,14 @@ nm_netns_init(NMNetns *self)
                                                        _ecmp_routes_by_ecmpid_equal,
                                                        _ecmp_routes_by_ecmpid_free,
                                                        NULL);
+
+    priv->watcher_idx = g_hash_table_new(_watcher_handle_hash, _watcher_handle_equal);
+    G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(WatcherByTag, tag) == 0);
+    priv->watcher_by_tag_idx  = g_hash_table_new_full(nm_pdirect_hash,
+                                                     nm_pdirect_equal,
+                                                     (GDestroyNotify) _watcher_by_tag_destroy,
+                                                     NULL);
+    priv->watcher_ip_data_idx = g_hash_table_new(_watcher_ip_data_hash, _watcher_ip_data_equal);
 }
 
 static void
@@ -937,10 +1517,17 @@ dispose(GObject *object)
     nm_assert(nm_g_hash_table_size(priv->l3cfgs) == 0);
     nm_assert(c_list_is_empty(&priv->l3cfg_signal_pending_lst_head));
     nm_assert(!priv->shared_ips);
+    nm_assert(nm_g_hash_table_size(priv->watcher_idx) == 0);
+    nm_assert(nm_g_hash_table_size(priv->watcher_by_tag_idx) == 0);
+    nm_assert(nm_g_hash_table_size(priv->watcher_ip_data_idx) == 0);
 
     nm_clear_pointer(&priv->ecmp_track_by_obj, g_hash_table_destroy);
     nm_clear_pointer(&priv->ecmp_track_by_ecmpid, g_hash_table_destroy);
 
+    nm_clear_pointer(&priv->watcher_idx, g_hash_table_destroy);
+    nm_clear_pointer(&priv->watcher_by_tag_idx, g_hash_table_destroy);
+    nm_clear_pointer(&priv->watcher_ip_data_idx, g_hash_table_destroy);
+
     nm_clear_g_source_inst(&priv->signal_pending_idle_source);
 
     if (priv->platform)
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 84a78f83..7725ae79 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -59,4 +59,46 @@ void nm_netns_ip_route_ecmp_commit(NMNetns    *self,
                                    GPtrArray **routes,
                                    gboolean    is_reapply);
 
+/*****************************************************************************/
+
+typedef enum {
+    NM_NETNS_WATCHER_TYPE_IP_ADDR,
+} NMNetnsWatcherType;
+
+typedef struct {
+    union {
+        struct {
+            NMIPAddrTyped addr;
+        } ip_addr;
+    };
+} NMNetnsWatcherData;
+
+typedef struct {
+    union {
+        struct {
+            const NMPObject           *obj;
+            NMPlatformSignalChangeType change_type;
+        } ip_addr;
+    };
+} NMNetnsWatcherEventData;
+
+typedef struct _NMNetnsWatcherHandle NMNetnsWatcherHandle;
+
+typedef void (*NMNetnsWatcherCallback)(NMNetns                       *self,
+                                       NMNetnsWatcherType             watcher_type,
+                                       const NMNetnsWatcherData      *watcher_data,
+                                       gconstpointer                  tag,
+                                       const NMNetnsWatcherEventData *event_data,
+                                       gpointer                       user_data);
+
+void nm_netns_watcher_add(NMNetns                  *self,
+                          NMNetnsWatcherType        watcher_type,
+                          const NMNetnsWatcherData *watcher_data,
+                          gconstpointer             tag,
+                          NMNetnsWatcherCallback    callback,
+                          gpointer                  user_data);
+
+void
+nm_netns_watcher_remove_all(NMNetns *self, gconstpointer tag, gboolean all /* or only dirty */);
+
 #endif /* __NM_NETNS_H__ */
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index d7e05b7b..efdb0636 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -51,7 +51,7 @@ typedef struct {
     NMManager          *manager;
     NMNetns            *netns;
     NMFirewalldManager *firewalld_manager;
-    CList               pending_activation_checks;
+    CList               policy_auto_activate_lst_head;
 
     NMAgentManager *agent_mgr;
 
@@ -62,6 +62,10 @@ typedef struct {
 
     NMSettings *settings;
 
+    GSource *device_recheck_auto_activate_all_idle_source;
+
+    GSource *reset_connections_retries_idle_source;
+
     NMHostnameManager *hostname_manager;
 
     NMActiveConnection *default_ac4, *activating_ac4;
@@ -70,10 +74,6 @@ typedef struct {
     NMDnsManager *dns_manager;
     gulong        config_changed_id;
 
-    guint reset_retries_id; /* idle handler for resetting the retries count */
-
-    guint schedule_activate_all_id; /* idle handler for schedule_activate_all(). */
-
     NMPolicyHostnameMode hostname_mode;
     char                *orig_hostname;     /* hostname at NM start time */
     char                *cur_hostname;      /* hostname we want to assign */
@@ -135,8 +135,7 @@ _PRIV_TO_SELF(NMPolicyPrivate *priv)
 /*****************************************************************************/
 
 static void      update_system_hostname(NMPolicy *self, const char *msg);
-static void      schedule_activate_all(NMPolicy *self);
-static void      schedule_activate_check(NMPolicy *self, NMDevice *device);
+static void      nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self);
 static NMDevice *get_default_device(NMPolicy *self, int addr_family);
 
 /*****************************************************************************/
@@ -1283,23 +1282,6 @@ check_activating_active_connections(NMPolicy *self)
     g_object_thaw_notify(G_OBJECT(self));
 }
 
-typedef struct {
-    CList     pending_lst;
-    NMPolicy *policy;
-    NMDevice *device;
-    guint     autoactivate_id;
-} ActivateData;
-
-static void
-activate_data_free(ActivateData *data)
-{
-    nm_device_remove_pending_action(data->device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);
-    c_list_unlink_stale(&data->pending_lst);
-    nm_clear_g_source(&data->autoactivate_id);
-    g_object_unref(data->device);
-    g_slice_free(ActivateData, data);
-}
-
 static void
 pending_ac_gone(gpointer data, GObject *where_the_object_was)
 {
@@ -1326,13 +1308,17 @@ pending_ac_state_changed(NMActiveConnection *ac, guint state, guint reason, NMPo
          * device, but block the current connection to avoid an activation
          * loop.
          */
-        if (reason != NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED) {
+        if (reason != NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED
+            && reason != NM_ACTIVE_CONNECTION_STATE_REASON_CONNECTION_REMOVED) {
             con = nm_active_connection_get_settings_connection(ac);
-            nm_settings_connection_autoconnect_blocked_reason_set(
+            nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                nm_active_connection_get_device(ac),
                 con,
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                 TRUE);
-            schedule_activate_check(self, nm_active_connection_get_device(ac));
+            nm_policy_device_recheck_auto_activate_schedule(self,
+                                                            nm_active_connection_get_device(ac));
         }
 
         /* Cleanup */
@@ -1345,7 +1331,7 @@ pending_ac_state_changed(NMActiveConnection *ac, guint state, guint reason, NMPo
 }
 
 static void
-auto_activate_device(NMPolicy *self, NMDevice *device)
+_auto_activate_device(NMPolicy *self, NMDevice *device)
 {
     NMPolicyPrivate               *priv;
     NMSettingsConnection          *best_connection;
@@ -1391,7 +1377,7 @@ auto_activate_device(NMPolicy *self, NMDevice *device)
         NMSettingConnection  *s_con;
         const char           *permission;
 
-        if (nm_settings_connection_autoconnect_is_blocked(candidate))
+        if (nm_manager_devcon_autoconnect_is_blocked(priv->manager, device, candidate))
             continue;
 
         cand_conn = nm_settings_connection_get_connection(candidate);
@@ -1435,11 +1421,13 @@ auto_activate_device(NMPolicy *self, NMDevice *device)
               "connection '%s' auto-activation failed: %s",
               nm_settings_connection_get_id(best_connection),
               error->message);
-        nm_settings_connection_autoconnect_blocked_reason_set(
+        nm_manager_devcon_autoconnect_blocked_reason_set(
+            priv->manager,
+            device,
             best_connection,
-            NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
             TRUE);
-        schedule_activate_check(self, device);
+        nm_policy_device_recheck_auto_activate_schedule(self, device);
         return;
     }
 
@@ -1456,32 +1444,33 @@ auto_activate_device(NMPolicy *self, NMDevice *device)
     }
 }
 
-static gboolean
-auto_activate_device_cb(gpointer user_data)
+static void
+_auto_activate_device_clear(NMPolicy *self, NMDevice *device, gboolean do_activate)
 {
-    ActivateData *data = user_data;
+    nm_assert(NM_IS_DEVICE(device));
+    nm_assert(NM_IS_POLICY(self));
+    nm_assert(c_list_is_linked(&device->policy_auto_activate_lst));
+    nm_assert(c_list_contains(&NM_POLICY_GET_PRIVATE(self)->policy_auto_activate_lst_head,
+                              &device->policy_auto_activate_lst));
 
-    g_assert(data);
-    g_assert(NM_IS_POLICY(data->policy));
-    g_assert(NM_IS_DEVICE(data->device));
+    c_list_unlink(&device->policy_auto_activate_lst);
+    nm_clear_g_source_inst(&device->policy_auto_activate_idle_source);
 
-    data->autoactivate_id = 0;
-    auto_activate_device(data->policy, data->device);
-    activate_data_free(data);
-    return G_SOURCE_REMOVE;
+    if (do_activate)
+        _auto_activate_device(self, device);
+
+    nm_device_remove_pending_action(device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);
 }
 
-static ActivateData *
-find_pending_activation(NMPolicy *self, NMDevice *device)
+static gboolean
+_auto_activate_idle_cb(gpointer user_data)
 {
-    NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
-    ActivateData    *data;
+    NMDevice *device = user_data;
 
-    c_list_for_each_entry (data, &priv->pending_activation_checks, pending_lst) {
-        if (data->device == device)
-            return data;
-    }
-    return NULL;
+    nm_assert(NM_IS_DEVICE(device));
+
+    _auto_activate_device_clear(nm_manager_get_policy(nm_device_get_manager(device)), device, TRUE);
+    return G_SOURCE_CONTINUE;
 }
 
 /*****************************************************************************/
@@ -1600,10 +1589,12 @@ nm_policy_unblock_failed_ovs_interfaces(NMPolicy *self)
         NMConnection         *connection = nm_settings_connection_get_connection(sett_conn);
 
         if (nm_connection_get_setting_ovs_interface(connection)) {
-            nm_settings_connection_autoconnect_retries_reset(sett_conn);
-            nm_settings_connection_autoconnect_blocked_reason_set(
+            nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn);
+            nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                NULL,
                 sett_conn,
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                 FALSE);
         }
     }
@@ -1634,36 +1625,15 @@ reset_autoconnect_all(
             && !nm_device_check_connection_compatible(
                 device,
                 nm_settings_connection_get_connection(sett_conn),
+                TRUE,
                 NULL))
             continue;
 
-        if (only_no_secrets) {
-            /* we only reset the no-secrets blocked flag. */
-            if (nm_settings_connection_autoconnect_blocked_reason_set(
-                    sett_conn,
-                    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS,
-                    FALSE)) {
-                /* maybe the connection is still blocked afterwards for other reasons
-                 * and in the larger picture nothing changed. But it's too complicated
-                 * to find out exactly. Just assume, something changed to be sure. */
-                if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
-                    changed = TRUE;
-            }
-        } else {
-            /* we reset the tries-count and any blocked-reason */
-            if (nm_settings_connection_autoconnect_retries_get(sett_conn) == 0)
-                changed = TRUE;
-            nm_settings_connection_autoconnect_retries_reset(sett_conn);
-
-            if (nm_settings_connection_autoconnect_blocked_reason_set(
-                    sett_conn,
-                    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_ALL
-                        & ~NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST,
-                    FALSE)) {
-                if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
-                    changed = TRUE;
-            }
-        }
+        if (nm_manager_devcon_autoconnect_reset_reconnect_all(priv->manager,
+                                                              device,
+                                                              sett_conn,
+                                                              only_no_secrets))
+            changed = TRUE;
     }
     return changed;
 }
@@ -1683,21 +1653,35 @@ sleeping_changed(NMManager *manager, GParamSpec *pspec, gpointer user_data)
         reset_autoconnect_all(self, NULL, FALSE);
 }
 
-static void
-schedule_activate_check(NMPolicy *self, NMDevice *device)
+void
+nm_policy_device_recheck_auto_activate_schedule(NMPolicy *self, NMDevice *device)
 {
-    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
-    ActivateData       *data;
+    NMPolicyPrivate    *priv;
     NMActiveConnection *ac;
     const CList        *tmp_list;
 
-    if (nm_manager_get_state(priv->manager) == NM_STATE_ASLEEP)
+    g_return_if_fail(NM_IS_POLICY(self));
+    g_return_if_fail(NM_IS_DEVICE(device));
+    nm_assert(g_signal_handler_find(device,
+                                    G_SIGNAL_MATCH_DATA,
+                                    0,
+                                    0,
+                                    NULL,
+                                    NULL,
+                                    NM_POLICY_GET_PRIVATE(self))
+              != 0);
+
+    if (!c_list_is_empty(&device->policy_auto_activate_lst)) {
+        /* already queued. Return. */
         return;
+    }
 
-    if (!nm_device_autoconnect_allowed(device))
+    priv = NM_POLICY_GET_PRIVATE(self);
+
+    if (nm_manager_get_state(priv->manager) == NM_STATE_ASLEEP)
         return;
 
-    if (find_pending_activation(self, device))
+    if (!nm_device_autoconnect_allowed(device))
         return;
 
     nm_manager_for_each_active_connection (priv->manager, ac, tmp_list) {
@@ -1712,11 +1696,8 @@ schedule_activate_check(NMPolicy *self, NMDevice *device)
 
     nm_device_add_pending_action(device, NM_PENDING_ACTION_AUTOACTIVATE, TRUE);
 
-    data                  = g_slice_new0(ActivateData);
-    data->policy          = self;
-    data->device          = g_object_ref(device);
-    data->autoactivate_id = g_idle_add(auto_activate_device_cb, data);
-    c_list_link_tail(&priv->pending_activation_checks, &data->pending_lst);
+    c_list_link_tail(&priv->policy_auto_activate_lst_head, &device->policy_auto_activate_lst);
+    device->policy_auto_activate_idle_source = nm_g_idle_add_source(_auto_activate_idle_cb, device);
 }
 
 static gboolean
@@ -1729,7 +1710,7 @@ reset_connections_retries(gpointer user_data)
     gint32                       con_stamp, min_stamp, now;
     gboolean                     changed = FALSE;
 
-    priv->reset_retries_id = 0;
+    nm_clear_g_source_inst(&priv->reset_connections_retries_idle_source);
 
     min_stamp   = 0;
     now         = nm_utils_get_monotonic_timestamp_sec();
@@ -1737,91 +1718,83 @@ reset_connections_retries(gpointer user_data)
     for (i = 0; connections[i]; i++) {
         NMSettingsConnection *connection = connections[i];
 
-        con_stamp = nm_settings_connection_autoconnect_retries_blocked_until(connection);
+        con_stamp =
+            nm_manager_devcon_autoconnect_retries_blocked_until(priv->manager, NULL, connection);
         if (con_stamp == 0)
             continue;
 
         if (con_stamp <= now) {
-            nm_settings_connection_autoconnect_retries_reset(connection);
+            nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, connection);
             changed = TRUE;
         } else if (min_stamp == 0 || min_stamp > con_stamp)
             min_stamp = con_stamp;
     }
 
     /* Schedule the handler again if there are some stamps left */
-    if (min_stamp != 0)
-        priv->reset_retries_id =
-            g_timeout_add_seconds(min_stamp - now, reset_connections_retries, self);
+    if (min_stamp != 0) {
+        priv->reset_connections_retries_idle_source =
+            nm_g_timeout_add_seconds_source(min_stamp - now, reset_connections_retries, self);
+    }
 
     /* If anything changed, try to activate the newly re-enabled connections */
     if (changed)
-        schedule_activate_all(self);
+        nm_policy_device_recheck_auto_activate_all_schedule(self);
 
-    return FALSE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
-_connection_autoconnect_retries_set(NMPolicy *self, NMSettingsConnection *connection, int tries)
+_connection_autoconnect_retries_set(NMPolicy             *self,
+                                    NMDevice             *device,
+                                    NMSettingsConnection *connection,
+                                    guint32               tries)
 {
     NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
 
     nm_assert(NM_IS_SETTINGS_CONNECTION(connection));
-    nm_assert(tries >= 0);
 
-    nm_settings_connection_autoconnect_retries_set(connection, tries);
+    nm_manager_devcon_autoconnect_retries_set(priv->manager, device, connection, tries);
 
     if (tries == 0) {
         /* Schedule a handler to reset retries count */
-        if (!priv->reset_retries_id) {
-            gint32 retry_time =
-                nm_settings_connection_autoconnect_retries_blocked_until(connection);
-
-            g_warn_if_fail(retry_time != 0);
-            priv->reset_retries_id =
-                g_timeout_add_seconds(MAX(0, retry_time - nm_utils_get_monotonic_timestamp_sec()),
-                                      reset_connections_retries,
-                                      self);
+        if (!priv->reset_connections_retries_idle_source) {
+            gint32 retry_time;
+
+            retry_time = nm_manager_devcon_autoconnect_retries_blocked_until(priv->manager,
+                                                                             device,
+                                                                             connection);
+            nm_assert(retry_time != 0);
+
+            priv->reset_connections_retries_idle_source = nm_g_timeout_add_seconds_source(
+                MAX(0, retry_time - nm_utils_get_monotonic_timestamp_sec()),
+                reset_connections_retries,
+                self);
         }
     }
 }
 
 static void
-activate_slave_connections(NMPolicy *self, NMDevice *device)
+unblock_autoconnect_for_ports(NMPolicy   *self,
+                              const char *master_device,
+                              const char *master_uuid_settings,
+                              const char *master_uuid_applied,
+                              gboolean    reset_devcon_autoconnect)
 {
     NMPolicyPrivate             *priv = NM_POLICY_GET_PRIVATE(self);
-    const char                  *master_device;
-    const char                  *master_uuid_settings = NULL;
-    const char                  *master_uuid_applied  = NULL;
-    guint                        i;
-    NMActRequest                *req;
-    gboolean                     internal_activation = FALSE;
     NMSettingsConnection *const *connections;
     gboolean                     changed;
+    guint                        i;
 
-    master_device = nm_device_get_iface(device);
-    g_assert(master_device);
-
-    req = nm_device_get_act_request(device);
-    if (req) {
-        NMConnection         *connection;
-        NMSettingsConnection *sett_conn;
-        NMAuthSubject        *subject;
-
-        connection = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(req));
-        if (connection)
-            master_uuid_applied = nm_connection_get_uuid(connection);
-
-        sett_conn = nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(req));
-        if (sett_conn) {
-            master_uuid_settings = nm_settings_connection_get_uuid(sett_conn);
-            if (nm_streq0(master_uuid_settings, master_uuid_applied))
-                master_uuid_settings = NULL;
-        }
-
-        subject = nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(req));
-        internal_activation =
-            subject && (nm_auth_subject_get_subject_type(subject) == NM_AUTH_SUBJECT_TYPE_INTERNAL);
-    }
+    _LOGT(LOGD_CORE,
+          "block-autoconnect: unblocking port profiles for controller ifname=%s%s%s, uuid=%s%s%s"
+          "%s%s%s",
+          NM_PRINT_FMT_QUOTE_STRING(master_device),
+          NM_PRINT_FMT_QUOTE_STRING(master_uuid_settings),
+          NM_PRINT_FMT_QUOTED(master_uuid_applied,
+                              ", applied-uuid=\"",
+                              master_uuid_applied,
+                              "\"",
+                              ""));
 
     changed     = FALSE;
     connections = nm_settings_get_connections(priv->settings, NULL);
@@ -1831,21 +1804,25 @@ activate_slave_connections(NMPolicy *self, NMDevice *device)
         const char           *slave_master;
 
         s_slave_con =
-            nm_connection_get_setting_connection(nm_settings_connection_get_connection(sett_conn));
+            nm_settings_connection_get_setting(sett_conn, NM_META_SETTING_TYPE_CONNECTION);
         slave_master = nm_setting_connection_get_master(s_slave_con);
         if (!slave_master)
             continue;
+
         if (!NM_IN_STRSET(slave_master, master_device, master_uuid_applied, master_uuid_settings))
             continue;
 
-        if (!internal_activation) {
-            if (nm_settings_connection_autoconnect_retries_get(sett_conn) == 0)
+        if (reset_devcon_autoconnect) {
+            if (nm_manager_devcon_autoconnect_retries_reset(priv->manager, NULL, sett_conn))
                 changed = TRUE;
-            nm_settings_connection_autoconnect_retries_reset(sett_conn);
         }
-        if (nm_settings_connection_autoconnect_blocked_reason_set(
+
+        /* unblock the devices associated with that connection */
+        if (nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                NULL,
                 sett_conn,
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                 FALSE)) {
             if (!nm_settings_connection_autoconnect_is_blocked(sett_conn))
                 changed = TRUE;
@@ -1853,7 +1830,68 @@ activate_slave_connections(NMPolicy *self, NMDevice *device)
     }
 
     if (changed)
-        schedule_activate_all(self);
+        nm_policy_device_recheck_auto_activate_all_schedule(self);
+}
+
+static void
+unblock_autoconnect_for_ports_for_sett_conn(NMPolicy *self, NMSettingsConnection *sett_conn)
+{
+    const char          *master_device;
+    const char          *master_uuid_settings;
+    NMSettingConnection *s_con;
+
+    nm_assert(NM_IS_POLICY(self));
+    nm_assert(NM_IS_SETTINGS_CONNECTION(sett_conn));
+
+    s_con = nm_settings_connection_get_setting(sett_conn, NM_META_SETTING_TYPE_CONNECTION);
+
+    nm_assert(NM_IS_SETTING_CONNECTION(s_con));
+
+    master_uuid_settings = nm_setting_connection_get_uuid(s_con);
+    master_device        = nm_setting_connection_get_interface_name(s_con);
+
+    unblock_autoconnect_for_ports(self, master_device, master_uuid_settings, NULL, TRUE);
+}
+
+static void
+activate_slave_connections(NMPolicy *self, NMDevice *device)
+{
+    const char   *master_device;
+    const char   *master_uuid_settings = NULL;
+    const char   *master_uuid_applied  = NULL;
+    NMActRequest *req;
+    gboolean      internal_activation = FALSE;
+
+    master_device = nm_device_get_iface(device);
+    nm_assert(master_device);
+
+    req = nm_device_get_act_request(device);
+    if (req) {
+        NMConnection         *connection;
+        NMSettingsConnection *sett_conn;
+        NMAuthSubject        *subject;
+
+        sett_conn = nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(req));
+        if (sett_conn)
+            master_uuid_settings = nm_settings_connection_get_uuid(sett_conn);
+
+        connection = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(req));
+        if (connection)
+            master_uuid_applied = nm_connection_get_uuid(connection);
+
+        if (nm_streq0(master_uuid_settings, master_uuid_applied))
+            master_uuid_applied = NULL;
+
+        subject = nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(req));
+        internal_activation =
+            subject && (nm_auth_subject_get_subject_type(subject) == NM_AUTH_SUBJECT_TYPE_INTERNAL);
+    }
+
+    unblock_autoconnect_for_ports(self,
+                                  master_device,
+                                  master_uuid_settings,
+                                  master_uuid_applied,
+                                  !internal_activation);
 }
 
 static gboolean
@@ -1968,9 +2006,11 @@ device_state_changed(NMDevice           *device,
          * a missing SIM or wrong modem initialization).
          */
         if (sett_conn) {
-            nm_settings_connection_autoconnect_blocked_reason_set(
+            nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                device,
                 sett_conn,
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                 TRUE);
         }
         break;
@@ -1988,10 +2028,10 @@ device_state_changed(NMDevice           *device,
         if (sett_conn && old_state >= NM_DEVICE_STATE_PREPARE
             && old_state <= NM_DEVICE_STATE_ACTIVATED) {
             gboolean blocked = FALSE;
-            int      tries;
             guint64  con_v;
 
-            if (nm_device_state_reason_check(reason) == NM_DEVICE_STATE_REASON_NO_SECRETS) {
+            switch (nm_device_state_reason_check(reason)) {
+            case NM_DEVICE_STATE_REASON_NO_SECRETS:
                 /* we want to block the connection from auto-connect if it failed due to no-secrets.
                  * However, if a secret-agent registered, since the connection made the last
                  * secret-request, we do not block it. The new secret-agent might not yet
@@ -2008,16 +2048,17 @@ device_state_changed(NMDevice           *device,
                 con_v = nm_settings_connection_get_last_secret_agent_version_id(sett_conn);
                 if (con_v == 0 || con_v == nm_agent_manager_get_agent_version_id(priv->agent_mgr)) {
                     _LOGD(LOGD_DEVICE,
-                          "connection '%s' now blocked from autoconnect due to no secrets",
+                          "block-autoconnect: connection '%s' now blocked from autoconnect due to "
+                          "no secrets",
                           nm_settings_connection_get_id(sett_conn));
                     nm_settings_connection_autoconnect_blocked_reason_set(
                         sett_conn,
-                        NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS,
+                        NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS,
                         TRUE);
                     blocked = TRUE;
                 }
-            } else if (nm_device_state_reason_check(reason)
-                       == NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED) {
+                break;
+            case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
                 /* A connection that fails due to dependency-failed is not
                  * able to reconnect until the master connection activates
                  * again; when this happens, the master clears the blocked
@@ -2027,26 +2068,41 @@ device_state_changed(NMDevice           *device,
                  * dependency-failed.
                  */
                 _LOGD(LOGD_DEVICE,
-                      "connection '%s' now blocked from autoconnect due to failed dependency",
+                      "block-autoconnect: connection[%p] (%s) now blocked from autoconnect due to "
+                      "failed "
+                      "dependency",
+                      sett_conn,
                       nm_settings_connection_get_id(sett_conn));
-                nm_settings_connection_autoconnect_blocked_reason_set(
+                nm_manager_devcon_autoconnect_blocked_reason_set(
+                    priv->manager,
+                    device,
                     sett_conn,
-                    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED,
+                    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                     TRUE);
                 blocked = TRUE;
+                break;
+            default:
+                break;
             }
 
             if (!blocked) {
-                tries = nm_settings_connection_autoconnect_retries_get(sett_conn);
-                if (tries > 0) {
+                guint32 tries;
+
+                tries = nm_manager_devcon_autoconnect_retries_get(priv->manager, device, sett_conn);
+                if (tries == 0) {
+                    /* blocked */
+                } else if (tries != NM_AUTOCONNECT_RETRIES_FOREVER) {
                     _LOGD(LOGD_DEVICE,
-                          "connection '%s' failed to autoconnect; %d tries left",
+                          "autoconnect: connection[%p] (%s): failed to autoconnect; %u tries left",
+                          sett_conn,
                           nm_settings_connection_get_id(sett_conn),
-                          tries - 1);
-                    _connection_autoconnect_retries_set(self, sett_conn, tries - 1);
-                } else if (tries != 0) {
+                          tries - 1u);
+                    _connection_autoconnect_retries_set(self, device, sett_conn, tries - 1u);
+                } else {
                     _LOGD(LOGD_DEVICE,
-                          "connection '%s' failed to autoconnect; infinite tries left",
+                          "autoconnect: connection[%p] (%s) failed to autoconnect; infinite tries "
+                          "left",
+                          sett_conn,
                           nm_settings_connection_get_id(sett_conn));
                 }
             }
@@ -2055,7 +2111,7 @@ device_state_changed(NMDevice           *device,
     case NM_DEVICE_STATE_ACTIVATED:
         if (sett_conn) {
             /* Reset auto retries back to default since connection was successful */
-            nm_settings_connection_autoconnect_retries_reset(sett_conn);
+            nm_manager_devcon_autoconnect_retries_reset(priv->manager, device, sett_conn);
         }
 
         /* Since there is no guarantee that device_l3cd_changed() is called
@@ -2087,27 +2143,34 @@ device_state_changed(NMDevice           *device,
     case NM_DEVICE_STATE_DEACTIVATING:
         if (sett_conn) {
             NMSettingsAutoconnectBlockedReason blocked_reason =
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE;
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE;
 
             switch (nm_device_state_reason_check(reason)) {
             case NM_DEVICE_STATE_REASON_USER_REQUESTED:
-                blocked_reason = NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST;
+                blocked_reason = NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST;
                 break;
             case NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED:
-                blocked_reason = NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED;
+                blocked_reason = NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED;
                 break;
             default:
                 break;
             }
-            if (blocked_reason != NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE) {
+            if (blocked_reason != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE) {
                 _LOGD(LOGD_DEVICE,
-                      "blocking autoconnect of connection '%s': %s",
+                      "block-autoconnect: blocking autoconnect of connection '%s': %s",
                       nm_settings_connection_get_id(sett_conn),
                       NM_UTILS_LOOKUP_STR_A(nm_device_state_reason_to_string,
                                             nm_device_state_reason_check(reason)));
-                nm_settings_connection_autoconnect_blocked_reason_set(sett_conn,
-                                                                      blocked_reason,
-                                                                      TRUE);
+                if (blocked_reason == NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED)
+                    nm_manager_devcon_autoconnect_blocked_reason_set(priv->manager,
+                                                                     device,
+                                                                     sett_conn,
+                                                                     blocked_reason,
+                                                                     TRUE);
+                else
+                    nm_settings_connection_autoconnect_blocked_reason_set(sett_conn,
+                                                                          blocked_reason,
+                                                                          TRUE);
             }
         }
         ip6_remove_device_prefix_delegations(self, device);
@@ -2126,7 +2189,7 @@ device_state_changed(NMDevice           *device,
             update_routing_and_dns(self, FALSE, device);
 
         /* Device is now available for auto-activation */
-        schedule_activate_check(self, device);
+        nm_policy_device_recheck_auto_activate_schedule(self, device);
         break;
 
     case NM_DEVICE_STATE_PREPARE:
@@ -2146,9 +2209,11 @@ device_state_changed(NMDevice           *device,
     case NM_DEVICE_STATE_IP_CONFIG:
         /* We must have secrets if we got here. */
         if (sett_conn)
-            nm_settings_connection_autoconnect_blocked_reason_set(
+            nm_manager_devcon_autoconnect_blocked_reason_set(
+                priv->manager,
+                device,
                 sett_conn,
-                NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_ALL,
+                NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_FAILED,
                 FALSE);
         break;
     case NM_DEVICE_STATE_SECONDARIES:
@@ -2248,16 +2313,7 @@ device_autoconnect_changed(NMDevice *device, GParamSpec *pspec, gpointer user_da
     NMPolicyPrivate *priv = user_data;
     NMPolicy        *self = _PRIV_TO_SELF(priv);
 
-    schedule_activate_check(self, device);
-}
-
-static void
-device_recheck_auto_activate(NMDevice *device, gpointer user_data)
-{
-    NMPolicyPrivate *priv = user_data;
-    NMPolicy        *self = _PRIV_TO_SELF(priv);
-
-    schedule_activate_check(self, device);
+    nm_policy_device_recheck_auto_activate_schedule(self, device);
 }
 
 static void
@@ -2292,10 +2348,6 @@ devices_list_register(NMPolicy *self, NMDevice *device)
                      "notify::" NM_DEVICE_AUTOCONNECT,
                      G_CALLBACK(device_autoconnect_changed),
                      priv);
-    g_signal_connect(device,
-                     NM_DEVICE_RECHECK_AUTO_ACTIVATE,
-                     G_CALLBACK(device_recheck_auto_activate),
-                     priv);
 }
 
 static void
@@ -2319,16 +2371,13 @@ device_removed(NMManager *manager, NMDevice *device, gpointer user_data)
 {
     NMPolicyPrivate *priv = user_data;
     NMPolicy        *self = _PRIV_TO_SELF(priv);
-    ActivateData    *data;
 
     /* TODO: is this needed? The delegations are cleaned up
      * on transition to deactivated too. */
     ip6_remove_device_prefix_delegations(self, device);
 
-    /* Clear any idle callbacks for this device */
-    data = find_pending_activation(self, device);
-    if (data && data->autoactivate_id)
-        activate_data_free(data);
+    if (c_list_is_linked(&device->policy_auto_activate_lst))
+        _auto_activate_device_clear(self, device, FALSE);
 
     if (g_hash_table_remove(priv->devices, device))
         devices_list_unregister(self, device);
@@ -2508,39 +2557,45 @@ active_connection_removed(NMManager *manager, NMActiveConnection *active, gpoint
 /*****************************************************************************/
 
 static gboolean
-schedule_activate_all_cb(gpointer user_data)
+_device_recheck_auto_activate_all_cb(gpointer user_data)
 {
     NMPolicy        *self = user_data;
     NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
     const CList     *tmp_lst;
     NMDevice        *device;
 
-    priv->schedule_activate_all_id = 0;
+    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);
 
     nm_manager_for_each_device (priv->manager, device, tmp_lst)
-        schedule_activate_check(self, device);
+        nm_policy_device_recheck_auto_activate_schedule(self, device);
 
-    return G_SOURCE_REMOVE;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
-schedule_activate_all(NMPolicy *self)
+nm_policy_device_recheck_auto_activate_all_schedule(NMPolicy *self)
 {
     NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
 
     /* always restart the idle handler. That way, we settle
      * all other events before restarting to activate them. */
-    nm_clear_g_source(&priv->schedule_activate_all_id);
-    priv->schedule_activate_all_id = g_idle_add(schedule_activate_all_cb, self);
+    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);
+
+    priv->device_recheck_auto_activate_all_idle_source =
+        nm_g_idle_add_source(_device_recheck_auto_activate_all_cb, self);
 }
 
+/*****************************************************************************/
+
 static void
 connection_added(NMSettings *settings, NMSettingsConnection *connection, gpointer user_data)
 {
     NMPolicyPrivate *priv = user_data;
     NMPolicy        *self = _PRIV_TO_SELF(priv);
 
-    schedule_activate_all(self);
+    unblock_autoconnect_for_ports_for_sett_conn(self, connection);
+
+    nm_policy_device_recheck_auto_activate_all_schedule(self);
 }
 
 static void
@@ -2597,6 +2652,8 @@ connection_updated(NMSettings           *settings,
     NMPolicy                        *self          = _PRIV_TO_SELF(priv);
     NMSettingsConnectionUpdateReason update_reason = update_reason_u;
 
+    unblock_autoconnect_for_ports_for_sett_conn(self, connection);
+
     if (NM_FLAGS_HAS(update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_REAPPLY_PARTIAL)) {
         const CList *tmp_lst;
         NMDevice    *device;
@@ -2608,44 +2665,15 @@ connection_updated(NMSettings           *settings,
         }
     }
 
-    schedule_activate_all(self);
-}
-
-static void
-_deactivate_if_active(NMPolicy *self, NMSettingsConnection *connection)
-{
-    NMPolicyPrivate    *priv = NM_POLICY_GET_PRIVATE(self);
-    NMActiveConnection *ac;
-    const CList        *tmp_list, *tmp_safe;
-    GError             *error = NULL;
-
-    nm_assert(NM_IS_SETTINGS_CONNECTION(connection));
-
-    nm_manager_for_each_active_connection_safe (priv->manager, ac, tmp_list, tmp_safe) {
-        if (nm_active_connection_get_settings_connection(ac) == connection
-            && (nm_active_connection_get_state(ac) <= NM_ACTIVE_CONNECTION_STATE_ACTIVATED)) {
-            if (!nm_manager_deactivate_connection(priv->manager,
-                                                  ac,
-                                                  NM_DEVICE_STATE_REASON_CONNECTION_REMOVED,
-                                                  &error)) {
-                _LOGW(LOGD_DEVICE,
-                      "connection '%s' disappeared, but error deactivating it: (%d) %s",
-                      nm_settings_connection_get_id(connection),
-                      error ? error->code : -1,
-                      error ? error->message : "(unknown)");
-                g_clear_error(&error);
-            }
-        }
-    }
+    nm_policy_device_recheck_auto_activate_all_schedule(self);
 }
 
 static void
 connection_removed(NMSettings *settings, NMSettingsConnection *connection, gpointer user_data)
 {
     NMPolicyPrivate *priv = user_data;
-    NMPolicy        *self = _PRIV_TO_SELF(priv);
 
-    _deactivate_if_active(self, connection);
+    nm_manager_deactivate_ac(priv->manager, connection);
 }
 
 static void
@@ -2657,7 +2685,7 @@ connection_flags_changed(NMSettings *settings, NMSettingsConnection *connection,
     if (NM_FLAGS_HAS(nm_settings_connection_get_flags(connection),
                      NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE)) {
         if (!nm_settings_connection_autoconnect_is_blocked(connection))
-            schedule_activate_all(self);
+            nm_policy_device_recheck_auto_activate_all_schedule(self);
     }
 }
 
@@ -2671,7 +2699,7 @@ secret_agent_registered(NMSettings *settings, NMSecretAgent *agent, gpointer use
      * connections failed due to missing secrets may re-try auto-connection.
      */
     if (reset_autoconnect_all(self, NULL, TRUE))
-        schedule_activate_all(self);
+        nm_policy_device_recheck_auto_activate_all_schedule(self);
 }
 
 NMActiveConnection *
@@ -2765,7 +2793,7 @@ nm_policy_init(NMPolicy *self)
     NMPolicyPrivate *priv          = NM_POLICY_GET_PRIVATE(self);
     gs_free char    *hostname_mode = NULL;
 
-    c_list_init(&priv->pending_activation_checks);
+    c_list_init(&priv->policy_auto_activate_lst_head);
 
     priv->netns = g_object_ref(nm_netns_get());
 
@@ -2901,9 +2929,9 @@ dispose(GObject *object)
 {
     NMPolicy        *self = NM_POLICY(object);
     NMPolicyPrivate *priv = NM_POLICY_GET_PRIVATE(self);
-    GHashTableIter   h_iter;
-    NMDevice        *device;
-    ActivateData    *data, *data_safe;
+
+    nm_assert(!c_list_is_empty(&priv->policy_auto_activate_lst_head));
+    nm_assert(g_hash_table_size(priv->devices) == 0);
 
     nm_clear_g_object(&priv->default_ac4);
     nm_clear_g_object(&priv->default_ac6);
@@ -2911,9 +2939,6 @@ dispose(GObject *object)
     nm_clear_g_object(&priv->activating_ac6);
     nm_clear_pointer(&priv->pending_active_connections, g_hash_table_unref);
 
-    c_list_for_each_entry_safe (data, data_safe, &priv->pending_activation_checks, pending_lst)
-        activate_data_free(data);
-
     g_slist_free_full(priv->pending_secondaries, (GDestroyNotify) pending_secondary_data_free);
     priv->pending_secondaries = NULL;
 
@@ -2932,20 +2957,14 @@ dispose(GObject *object)
         g_clear_object(&priv->dns_manager);
     }
 
-    g_hash_table_iter_init(&h_iter, priv->devices);
-    while (g_hash_table_iter_next(&h_iter, (gpointer *) &device, NULL)) {
-        g_hash_table_iter_remove(&h_iter);
-        devices_list_unregister(self, device);
-    }
-
     /* The manager should have disposed of ActiveConnections already, which
      * will have called active_connection_removed() and thus we don't need
      * to clean anything up.  Assert that this is TRUE.
      */
     nm_assert(c_list_is_empty(nm_manager_get_active_connections(priv->manager)));
 
-    nm_clear_g_source(&priv->reset_retries_id);
-    nm_clear_g_source(&priv->schedule_activate_all_id);
+    nm_clear_g_source_inst(&priv->reset_connections_retries_idle_source);
+    nm_clear_g_source_inst(&priv->device_recheck_auto_activate_all_idle_source);
 
     nm_clear_g_free(&priv->orig_hostname);
     nm_clear_g_free(&priv->cur_hostname);
diff --git a/src/core/nm-policy.h b/src/core/nm-policy.h
index f2b98dbd..9cfb0b24 100644
--- a/src/core/nm-policy.h
+++ b/src/core/nm-policy.h
@@ -34,6 +34,8 @@ NMActiveConnection *nm_policy_get_activating_ip6_ac(NMPolicy *policy);
 
 void nm_policy_unblock_failed_ovs_interfaces(NMPolicy *self);
 
+void nm_policy_device_recheck_auto_activate_schedule(NMPolicy *self, NMDevice *device);
+
 /**
  * NMPolicyHostnameMode
  * @NM_POLICY_HOSTNAME_MODE_NONE: never update the transient hostname.
diff --git a/src/core/nm-test-utils-core.h b/src/core/nm-test-utils-core.h
index e341d62f..176f4320 100644
--- a/src/core/nm-test-utils-core.h
+++ b/src/core/nm-test-utils-core.h
@@ -49,8 +49,8 @@ nmtst_platform_ip4_address_full(const char      *address,
 {
     NMPlatformIP4Address *addr = nmtst_platform_ip4_address(address, peer_address, plen);
 
-    G_STATIC_ASSERT(NMP_IFNAMSIZ == sizeof(addr->label));
-    g_assert(!label || strlen(label) < NMP_IFNAMSIZ);
+    G_STATIC_ASSERT(NM_IFNAMSIZ == sizeof(addr->label));
+    g_assert(!label || strlen(label) < NM_IFNAMSIZ);
 
     addr->ifindex     = ifindex;
     addr->addr_source = source;
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index 0c366d82..f6f377d4 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -102,7 +102,9 @@ static gboolean ip6_address_add(NMPlatform     *platform,
                                 struct in6_addr peer_addr,
                                 guint32         lifetime,
                                 guint32         preferred,
-                                guint           flags);
+                                guint           flags,
+                                char          **out_extack_msg);
+
 static gboolean
 ip6_address_delete(NMPlatform *platform, int ifindex, struct in6_addr addr, guint8 plen);
 
@@ -276,6 +278,24 @@ link_add_pre(NMPlatform *platform,
     return device;
 }
 
+static void
+link_add_post(NMPlatform *self, NMFakePlatformLink *device)
+{
+    char path[128];
+
+    switch (device->obj->link.type) {
+    case NM_LINK_TYPE_BRIDGE:
+        nm_sprintf_buf(path, "/sys/class/net/%s/bridge/default_pvid", device->obj->link.name);
+        sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), "1");
+
+        nm_sprintf_buf(path, "/sys/class/net/%s/bridge/vlan_filtering", device->obj->link.name);
+        sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), "0");
+        break;
+    default:
+        break;
+    }
+}
+
 static int
 link_add(NMPlatform            *platform,
          NMLinkType             type,
@@ -387,6 +407,7 @@ link_add(NMPlatform            *platform,
         *out_link = NMP_OBJECT_CAST_LINK(device->obj);
 
     link_changed(platform, device, cache_op, NULL);
+    link_add_post(platform, device);
     if (veth_peer)
         link_changed(platform, device_veth, cache_op_veth, NULL);
 
@@ -439,6 +460,7 @@ link_add_one(NMPlatform *platform,
 static gboolean
 link_delete(NMPlatform *platform, int ifindex)
 {
+    NMFakePlatformPrivate          *priv     = NM_FAKE_PLATFORM_GET_PRIVATE(platform);
     NMFakePlatformLink             *device   = link_get(platform, ifindex);
     nm_auto_nmpobj const NMPObject *obj_old  = NULL;
     nm_auto_nmpobj const NMPObject *obj_old2 = NULL;
@@ -449,6 +471,17 @@ link_delete(NMPlatform *platform, int ifindex)
 
     obj_old = g_steal_pointer(&device->obj);
 
+    if (obj_old->link.type == NM_LINK_TYPE_BRIDGE) {
+        char path[128];
+
+        g_hash_table_remove(
+            priv->options,
+            nm_sprintf_buf(path, "/sys/class/net/%s/bridge/default_pvid", obj_old->link.name));
+        g_hash_table_remove(
+            priv->options,
+            nm_sprintf_buf(path, "/sys/class/net/%s/bridge/vlan_filtering", obj_old->link.name));
+    }
+
     cache_op = nmp_cache_remove(nm_platform_get_cache(platform), obj_old, FALSE, FALSE, &obj_old2);
     g_assert(cache_op == NMP_CACHE_OPS_REMOVED);
     g_assert(obj_old2);
@@ -542,7 +575,7 @@ link_changed(NMPlatform         *platform,
     nm_platform_cache_update_emit_signal(platform, cache_op, obj_old, device->obj);
 
     if (!IN6_IS_ADDR_UNSPECIFIED(&device->ip6_lladdr)) {
-        if (device->obj->link.connected)
+        if (device->obj->link.connected) {
             ip6_address_add(platform,
                             device->obj->link.ifindex,
                             device->ip6_lladdr,
@@ -550,8 +583,9 @@ link_changed(NMPlatform         *platform,
                             in6addr_any,
                             NM_PLATFORM_LIFETIME_PERMANENT,
                             NM_PLATFORM_LIFETIME_PERMANENT,
-                            0);
-        else
+                            0,
+                            NULL);
+        } else
             ip6_address_delete(platform, device->obj->link.ifindex, device->ip6_lladdr, 64);
     }
 
@@ -669,8 +703,10 @@ link_supports_sriov(NMPlatform *platform, int ifindex)
 static gboolean
 link_change(NMPlatform                   *platform,
             int                           ifindex,
+            NMPlatformLinkProps          *props,
             NMPortKind                    port_kind,
-            const NMPlatformLinkPortData *port_data)
+            const NMPlatformLinkPortData *port_data,
+            NMPlatformLinkChangeFlags     flags)
 {
     NMFakePlatformLink       *device  = link_get(platform, ifindex);
     nm_auto_nmpobj NMPObject *obj_tmp = NULL;
@@ -747,6 +783,34 @@ link_vlan_change(NMPlatform             *platform,
     return FALSE;
 }
 
+static gboolean
+link_set_bridge_info(NMPlatform                            *self,
+                     int                                    ifindex,
+                     const NMPlatformLinkSetBridgeInfoData *bridge_info)
+{
+    NMFakePlatformLink *link;
+    char                path[128];
+    char                value[128];
+
+    link = link_get(self, ifindex);
+    if (!link)
+        return FALSE;
+
+    if (bridge_info->vlan_default_pvid_has) {
+        nm_sprintf_buf(path, "/sys/class/net/%s/bridge/default_pvid", link->obj->link.name);
+        nm_sprintf_buf(value, "%u", bridge_info->vlan_default_pvid_val);
+        sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), value);
+    }
+
+    if (bridge_info->vlan_filtering_has) {
+        nm_sprintf_buf(path, "/sys/class/net/%s/bridge/vlan_filtering", link->obj->link.name);
+        nm_sprintf_buf(value, "%u", bridge_info->vlan_filtering_val);
+        sysctl_set(self, NMP_SYSCTL_PATHID_ABSOLUTE(path), value);
+    }
+
+    return TRUE;
+}
+
 struct infiniband_add_data {
     int parent;
     int p_key;
@@ -785,7 +849,7 @@ infiniband_partition_add(NMPlatform            *platform,
     parent_device = link_get(platform, parent);
     g_return_val_if_fail(parent_device != NULL, FALSE);
 
-    nmp_utils_new_infiniband_name(name, parent_device->obj->link.name, p_key);
+    nm_net_devname_infiniband(name, parent_device->obj->link.name, p_key);
 
     link_add_one(platform, name, NM_LINK_TYPE_INFINIBAND, _infiniband_add_prepare, &d, out_link);
     return TRUE;
@@ -800,7 +864,7 @@ infiniband_partition_delete(NMPlatform *platform, int parent, int p_key)
     parent_device = link_get(platform, parent);
     g_return_val_if_fail(parent_device != NULL, FALSE);
 
-    nmp_utils_new_infiniband_name(name, parent_device->obj->link.name, p_key);
+    nm_net_devname_infiniband(name, parent_device->obj->link.name, p_key);
     return link_delete(platform, nm_platform_link_get_ifindex(platform, name));
 }
 
@@ -890,7 +954,10 @@ mesh_set_ssid(NMPlatform *platform, int ifindex, const guint8 *ssid, gsize len)
 /*****************************************************************************/
 
 static gboolean
-ipx_address_add(NMPlatform *platform, int addr_family, const NMPlatformObject *address)
+ipx_address_add(NMPlatform             *platform,
+                int                     addr_family,
+                const NMPlatformObject *address,
+                char                  **out_extack_msg)
 {
     nm_auto_nmpobj NMPObject       *obj = NULL;
     NMPCacheOpsType                 cache_op;
@@ -899,6 +966,7 @@ ipx_address_add(NMPlatform *platform, int addr_family, const NMPlatformObject *a
     NMPCache                       *cache   = nm_platform_get_cache(platform);
 
     g_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));
+    g_assert(!out_extack_msg || !*out_extack_msg);
 
     obj = nmp_object_new(addr_family == AF_INET ? NMP_OBJECT_TYPE_IP4_ADDRESS
                                                 : NMP_OBJECT_TYPE_IP6_ADDRESS,
@@ -919,7 +987,8 @@ ip4_address_add(NMPlatform *platform,
                 guint32     lifetime,
                 guint32     preferred,
                 guint32     flags,
-                const char *label)
+                const char *label,
+                char      **out_extack_msg)
 {
     NMPlatformIP4Address address;
 
@@ -939,7 +1008,7 @@ ip4_address_add(NMPlatform *platform,
     if (label)
         g_strlcpy(address.label, label, sizeof(address.label));
 
-    return ipx_address_add(platform, AF_INET, (const NMPlatformObject *) &address);
+    return ipx_address_add(platform, AF_INET, (const NMPlatformObject *) &address, out_extack_msg);
 }
 
 static gboolean
@@ -950,7 +1019,8 @@ ip6_address_add(NMPlatform     *platform,
                 struct in6_addr peer_addr,
                 guint32         lifetime,
                 guint32         preferred,
-                guint32         flags)
+                guint32         flags,
+                char          **out_extack_msg)
 {
     NMPlatformIP6Address address;
 
@@ -967,7 +1037,7 @@ ip6_address_add(NMPlatform     *platform,
     address.preferred   = preferred;
     address.n_ifa_flags = flags;
 
-    return ipx_address_add(platform, AF_INET6, (const NMPlatformObject *) &address);
+    return ipx_address_add(platform, AF_INET6, (const NMPlatformObject *) &address, out_extack_msg);
 }
 
 static gboolean
@@ -1117,7 +1187,7 @@ object_delete(NMPlatform *platform, const NMPObject *obj)
 }
 
 static int
-ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack)
+ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack, char **out_extack_msg)
 {
     NMDedupMultiIter                iter;
     nm_auto_nmpobj NMPObject       *obj = NULL;
@@ -1139,6 +1209,7 @@ ip_route_add(NMPlatform *platform, NMPNlmFlags flags, NMPObject *obj_stack)
     g_assert(NM_IN_SET(NMP_OBJECT_GET_TYPE(obj_stack),
                        NMP_OBJECT_TYPE_IP4_ROUTE,
                        NMP_OBJECT_TYPE_IP6_ROUTE));
+    g_assert(!out_extack_msg || !*out_extack_msg);
 
     addr_family = NMP_OBJECT_GET_ADDR_FAMILY(obj_stack);
 
@@ -1364,6 +1435,8 @@ nm_fake_platform_class_init(NMFakePlatformClass *klass)
 
     platform_class->link_vlan_change = link_vlan_change;
 
+    platform_class->link_set_bridge_info = link_set_bridge_info;
+
     platform_class->infiniband_partition_add    = infiniband_partition_add;
     platform_class->infiniband_partition_delete = infiniband_partition_delete;
 
diff --git a/src/core/platform/tests/test-cleanup.c b/src/core/platform/tests/test-cleanup.c
index 139a0280..c643e71c 100644
--- a/src/core/platform/tests/test-cleanup.c
+++ b/src/core/platform/tests/test-cleanup.c
@@ -77,6 +77,7 @@ test_cleanup_internal(void)
                                          lifetime,
                                          preferred,
                                          0,
+                                         NULL,
                                          NULL));
     g_assert(nm_platform_ip6_address_add(NM_PLATFORM_GET,
                                          ifindex,
@@ -85,7 +86,8 @@ test_cleanup_internal(void)
                                          in6addr_any,
                                          lifetime,
                                          preferred,
-                                         flags));
+                                         flags,
+                                         NULL));
     nmtstp_ip4_route_add(NM_PLATFORM_GET,
                          ifindex,
                          NM_IP_CONFIG_SOURCE_USER,
diff --git a/src/core/platform/tests/test-common.c b/src/core/platform/tests/test-common.c
index 65bdfeae..fde7dc0d 100644
--- a/src/core/platform/tests/test-common.c
+++ b/src/core/platform/tests/test-common.c
@@ -1809,7 +1809,8 @@ _ip_address_add(NMPlatform     *platform,
                                                   lifetime,
                                                   preferred,
                                                   flags,
-                                                  label);
+                                                  label,
+                                                  NULL);
         } else {
             g_assert(label == NULL);
             success = nm_platform_ip6_address_add(platform,
@@ -1819,7 +1820,8 @@ _ip_address_add(NMPlatform     *platform,
                                                   peer_address->addr6,
                                                   lifetime,
                                                   preferred,
-                                                  flags);
+                                                  flags,
+                                                  NULL);
         }
         g_assert(success);
     }
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index 33858671..8a54ac48 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -36,6 +36,16 @@
 #define _ADD_DUMMY(platform, name) \
     g_assert(NMTST_NM_ERR_SUCCESS(nm_platform_link_dummy_add((platform), (name), NULL)))
 
+#define _sysctl_assert_eq(plat, path, value)                                   \
+    G_STMT_START                                                               \
+    {                                                                          \
+        gs_free char *_val = NULL;                                             \
+                                                                               \
+        _val = nm_platform_sysctl_get(plat, NMP_SYSCTL_PATHID_ABSOLUTE(path)); \
+        g_assert_cmpstr(_val, ==, value);                                      \
+    }                                                                          \
+    G_STMT_END
+
 static void
 test_bogus(void)
 {
@@ -212,58 +222,58 @@ test_link_changed_signal_cb(NMPlatform               *platform,
 }
 
 static void
-test_slave(int master, int type, SignalData *master_changed)
+test_port(int controller, int port_type, SignalData *controller_changed)
 {
-    int         ifindex;
+    int         ifindex_port;
     SignalData *link_added = add_signal_ifname(NM_PLATFORM_SIGNAL_LINK_CHANGED,
                                                NM_PLATFORM_SIGNAL_ADDED,
                                                link_callback,
                                                SLAVE_NAME);
     SignalData *link_changed, *link_removed;
     char       *value;
-    NMLinkType  link_type = nm_platform_link_get_type(NM_PLATFORM_GET, master);
+    NMLinkType  controller_type = nm_platform_link_get_type(NM_PLATFORM_GET, controller);
     gboolean    test_link_changed_signal_arg1;
     gboolean    test_link_changed_signal_arg2;
 
-    g_assert(NM_IN_SET(link_type, NM_LINK_TYPE_TEAM, NM_LINK_TYPE_BOND, NM_LINK_TYPE_BRIDGE));
+    g_assert(NM_IN_SET(controller_type, NM_LINK_TYPE_TEAM, NM_LINK_TYPE_BOND, NM_LINK_TYPE_BRIDGE));
 
-    g_assert(software_add(type, SLAVE_NAME));
-    ifindex = nm_platform_link_get_ifindex(NM_PLATFORM_GET, SLAVE_NAME);
-    g_assert(ifindex > 0);
+    g_assert(software_add(port_type, SLAVE_NAME));
+    ifindex_port = nm_platform_link_get_ifindex(NM_PLATFORM_GET, SLAVE_NAME);
+    g_assert(ifindex_port > 0);
     link_changed = add_signal_ifindex(NM_PLATFORM_SIGNAL_LINK_CHANGED,
                                       NM_PLATFORM_SIGNAL_CHANGED,
                                       link_callback,
-                                      ifindex);
+                                      ifindex_port);
     link_removed = add_signal_ifindex(NM_PLATFORM_SIGNAL_LINK_CHANGED,
                                       NM_PLATFORM_SIGNAL_REMOVED,
                                       link_callback,
-                                      ifindex);
+                                      ifindex_port);
     accept_signal(link_added);
 
-    /* Set the slave up to see whether master's IFF_LOWER_UP is set correctly.
+    /* Set the port up to see whether controller's IFF_LOWER_UP is set correctly.
      *
      * See https://bugzilla.redhat.com/show_bug.cgi?id=910348
      */
-    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex));
-    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex, IFF_UP, FALSE) >= 0);
-    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex));
+    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex_port));
+    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex_port, IFF_UP, FALSE) >= 0);
+    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex_port));
     ensure_no_signal(link_changed);
 
-    /* Enslave */
-    link_changed->ifindex = ifindex;
-    g_assert(nm_platform_link_enslave(NM_PLATFORM_GET, master, ifindex));
-    g_assert_cmpint(nm_platform_link_get_master(NM_PLATFORM_GET, ifindex), ==, master);
+    /* Attach port */
+    link_changed->ifindex = ifindex_port;
+    g_assert(nm_platform_link_enslave(NM_PLATFORM_GET, controller, ifindex_port));
+    g_assert_cmpint(nm_platform_link_get_master(NM_PLATFORM_GET, ifindex_port), ==, controller);
 
     accept_signals(link_changed, 1, 3);
-    accept_signals(master_changed, 0, 2);
+    accept_signals(controller_changed, 0, 2);
 
-    /* enslaveing brings put the slave */
-    if (NM_IN_SET(link_type, NM_LINK_TYPE_BOND, NM_LINK_TYPE_TEAM))
-        g_assert(nm_platform_link_is_up(NM_PLATFORM_GET, ifindex));
+    /* Attaching ports brings up the port */
+    if (NM_IN_SET(controller_type, NM_LINK_TYPE_BOND, NM_LINK_TYPE_TEAM))
+        g_assert(nm_platform_link_is_up(NM_PLATFORM_GET, ifindex_port));
     else
-        g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex));
+        g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex_port));
 
-    if (NM_IN_SET(link_type, NM_LINK_TYPE_BOND)) {
+    if (NM_IN_SET(controller_type, NM_LINK_TYPE_BOND)) {
         NMPlatformLinkBondPort   bond_port;
         gboolean                 prio_has;
         gboolean                 prio_supported;
@@ -273,7 +283,7 @@ test_slave(int master, int type, SignalData *master_changed)
         link = nmtstp_link_get_typed(NM_PLATFORM_GET, 0, SLAVE_NAME, NM_LINK_TYPE_DUMMY);
         g_assert(link);
 
-        lnk = nm_platform_link_get_lnk_bond(NM_PLATFORM_GET, master, NULL);
+        lnk = nm_platform_link_get_lnk_bond(NM_PLATFORM_GET, controller, NULL);
         g_assert(lnk);
 
         g_assert(NM_IN_SET(lnk->mode, 3, 1));
@@ -286,13 +296,24 @@ test_slave(int master, int type, SignalData *master_changed)
             .prio     = prio_has ? 6 : 0,
         };
 
-        g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex, &bond_port));
+        g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex_port, NULL, &bond_port, 0));
         accept_signals(link_changed, 1, 3);
 
-        link = nmtstp_link_get(NM_PLATFORM_GET, ifindex, SLAVE_NAME);
+        link = nmtstp_link_get(NM_PLATFORM_GET, ifindex_port, SLAVE_NAME);
         g_assert(link);
         g_assert_cmpint(link->port_data.bond.queue_id, ==, 5);
         g_assert(link->port_data.bond.prio_has || link->port_data.bond.prio == 0);
+    } else if (controller_type == NM_LINK_TYPE_BRIDGE) {
+        /* Skip this part for nm-fake-platform */
+        if (nmtstp_is_root_test() && nmtstp_is_sysfs_writable()) {
+            g_assert(nm_platform_sysctl_slave_set_option(NM_PLATFORM_GET,
+                                                         ifindex_port,
+                                                         "priority",
+                                                         "614"));
+            value = nm_platform_sysctl_slave_get_option(NM_PLATFORM_GET, ifindex_port, "priority");
+            g_assert_cmpstr(value, ==, "614");
+            g_free(value);
+        }
     }
 
     test_link_changed_signal_arg1 = FALSE;
@@ -306,10 +327,10 @@ test_slave(int master, int type, SignalData *master_changed)
                      G_CALLBACK(test_link_changed_signal_cb),
                      &test_link_changed_signal_arg2);
 
-    /* Set master up */
-    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, master, IFF_UP, TRUE) >= 0);
-    g_assert(nm_platform_link_is_up(NM_PLATFORM_GET, master));
-    accept_signals(master_changed, 1, 3);
+    /* Set controller up */
+    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, controller, IFF_UP, TRUE) >= 0);
+    g_assert(nm_platform_link_is_up(NM_PLATFORM_GET, controller));
+    accept_signals(controller_changed, 1, 3);
 
     g_signal_handlers_disconnect_by_func(NM_PLATFORM_GET,
                                          G_CALLBACK(test_link_changed_signal_cb),
@@ -320,28 +341,28 @@ test_slave(int master, int type, SignalData *master_changed)
     g_assert(test_link_changed_signal_arg1);
     g_assert(test_link_changed_signal_arg2);
 
-    /* Master with a disconnected slave is disconnected
+    /* Master with a disconnected port is disconnected
      *
-     * For some reason, bonding and teaming slaves are automatically set up. We
+     * For some reason, bonding and teaming ports are automatically set up. We
      * need to set them back down for this test.
      */
-    switch (nm_platform_link_get_type(NM_PLATFORM_GET, master)) {
+    switch (nm_platform_link_get_type(NM_PLATFORM_GET, controller)) {
     case NM_LINK_TYPE_BOND:
     case NM_LINK_TYPE_TEAM:
-        g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex, IFF_UP, FALSE) >= 0);
+        g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex_port, IFF_UP, FALSE) >= 0);
         accept_signal(link_changed);
-        accept_signals(master_changed, 0, 3);
+        accept_signals(controller_changed, 0, 3);
         break;
     default:
         break;
     }
-    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex));
-    g_assert(!nm_platform_link_is_connected(NM_PLATFORM_GET, ifindex));
-    if (nmtstp_is_root_test() && nm_platform_link_is_connected(NM_PLATFORM_GET, master)) {
-        if (nm_platform_link_get_type(NM_PLATFORM_GET, master) == NM_LINK_TYPE_TEAM) {
-            /* Older team versions (e.g. Fedora 17) have a bug that team master stays
-             * IFF_LOWER_UP even if its slave is down. Double check it with iproute2 and if
-             * `ip link` also claims master to be up, accept it. */
+    g_assert(!nm_platform_link_is_up(NM_PLATFORM_GET, ifindex_port));
+    g_assert(!nm_platform_link_is_connected(NM_PLATFORM_GET, ifindex_port));
+    if (nmtstp_is_root_test() && nm_platform_link_is_connected(NM_PLATFORM_GET, controller)) {
+        if (nm_platform_link_get_type(NM_PLATFORM_GET, controller) == NM_LINK_TYPE_TEAM) {
+            /* Older team versions (e.g. Fedora 17) have a bug that team controller stays
+             * IFF_LOWER_UP even if its port is down. Double check it with iproute2 and if
+             * `ip link` also claims controller to be up, accept it. */
             char *stdout_str = NULL;
 
             nmtst_spawn_sync(NULL,
@@ -352,7 +373,7 @@ test_slave(int master, int type, SignalData *master_changed)
                              "link",
                              "show",
                              "dev",
-                             nm_platform_link_get_name(NM_PLATFORM_GET, master));
+                             nm_platform_link_get_name(NM_PLATFORM_GET, controller));
 
             g_assert(strstr(stdout_str, "LOWER_UP"));
             g_free(stdout_str);
@@ -360,44 +381,29 @@ test_slave(int master, int type, SignalData *master_changed)
             g_assert_not_reached();
     }
 
-    /* Set slave up and see if master gets up too */
-    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex, IFF_UP, TRUE) >= 0);
-    g_assert(nm_platform_link_is_connected(NM_PLATFORM_GET, ifindex));
-    g_assert(nm_platform_link_is_connected(NM_PLATFORM_GET, master));
+    /* Set port up and see if controller gets up too */
+    g_assert(nm_platform_link_change_flags(NM_PLATFORM_GET, ifindex_port, IFF_UP, TRUE) >= 0);
+    g_assert(nm_platform_link_is_connected(NM_PLATFORM_GET, ifindex_port));
+    g_assert(nm_platform_link_is_connected(NM_PLATFORM_GET, controller));
     accept_signals(link_changed, 1, 3);
     /* NM running, can cause additional change of addrgenmode */
-    accept_signals(master_changed, 0, 3);
+    accept_signals(controller_changed, 0, 3);
 
-    /* Enslave again
+    /* Attach port again
      *
-     * Gracefully succeed if already enslaved.
+     * Gracefully succeed if already attached port.
      */
     ensure_no_signal(link_changed);
-    g_assert(nm_platform_link_enslave(NM_PLATFORM_GET, master, ifindex));
+    g_assert(nm_platform_link_enslave(NM_PLATFORM_GET, controller, ifindex_port));
     accept_signals(link_changed, 0, 2);
-    accept_signals(master_changed, 0, 2);
-
-    /* Set slave option */
-    switch (type) {
-    case NM_LINK_TYPE_BRIDGE:
-        if (nmtstp_is_sysfs_writable()) {
-            g_assert(
-                nm_platform_sysctl_slave_set_option(NM_PLATFORM_GET, ifindex, "priority", "614"));
-            value = nm_platform_sysctl_slave_get_option(NM_PLATFORM_GET, ifindex, "priority");
-            g_assert_cmpstr(value, ==, "614");
-            g_free(value);
-        }
-        break;
-    default:
-        break;
-    }
+    accept_signals(controller_changed, 0, 2);
 
     /* Release */
     ensure_no_signal(link_added);
     ensure_no_signal(link_changed);
     ensure_no_signal(link_removed);
-    g_assert(nm_platform_link_release(NM_PLATFORM_GET, master, ifindex));
-    g_assert_cmpint(nm_platform_link_get_master(NM_PLATFORM_GET, ifindex), ==, 0);
+    g_assert(nm_platform_link_release(NM_PLATFORM_GET, controller, ifindex_port));
+    g_assert_cmpint(nm_platform_link_get_master(NM_PLATFORM_GET, ifindex_port), ==, 0);
     if (link_changed->received_count > 0) {
         accept_signals(link_added, 0, 1);
         accept_signals(link_changed, 1, 5);
@@ -409,22 +415,22 @@ test_slave(int master, int type, SignalData *master_changed)
         ensure_no_signal(link_changed);
         accept_signal(link_removed);
     }
-    accept_signals(master_changed, 0, 3);
+    accept_signals(controller_changed, 0, 3);
 
-    ensure_no_signal(master_changed);
+    ensure_no_signal(controller_changed);
 
     /* Release again */
     ensure_no_signal(link_changed);
-    g_assert(!nm_platform_link_release(NM_PLATFORM_GET, master, ifindex));
+    g_assert(!nm_platform_link_release(NM_PLATFORM_GET, controller, ifindex_port));
 
-    ensure_no_signal(master_changed);
+    ensure_no_signal(controller_changed);
 
     /* Remove */
     ensure_no_signal(link_added);
     ensure_no_signal(link_changed);
     ensure_no_signal(link_removed);
-    nmtstp_link_delete(NULL, -1, ifindex, NULL, TRUE);
-    accept_signals(master_changed, 0, 1);
+    nmtstp_link_delete(NULL, -1, ifindex_port, NULL, TRUE);
+    accept_signals(controller_changed, 0, 1);
     accept_signals(link_changed, 0, 1);
     accept_signal(link_removed);
 
@@ -528,7 +534,7 @@ test_software(NMLinkType link_type, const char *link_typename)
     case NM_LINK_TYPE_BOND:
     case NM_LINK_TYPE_TEAM:
         link_changed->ifindex = ifindex;
-        test_slave(ifindex, NM_LINK_TYPE_DUMMY, link_changed);
+        test_port(ifindex, NM_LINK_TYPE_DUMMY, link_changed);
         link_changed->ifindex = 0;
         break;
     default:
@@ -618,10 +624,14 @@ test_vlan(void)
 static void
 test_bridge_addr(void)
 {
-    char                  addr[ETH_ALEN];
-    NMPlatformLink        link;
-    const NMPlatformLink *plink = NULL;
-    NMPLinkAddress        hw_perm_addr;
+    char                            addr[ETH_ALEN];
+    NMPlatformLink                  link;
+    const NMPlatformLink           *plink = NULL;
+    NMPLinkAddress                  hw_perm_addr;
+    gboolean                        b;
+    char                            sbuf[100];
+    gs_free char                   *str = NULL;
+    NMPlatformLinkSetBridgeInfoData info_data;
 
     nm_utils_hwaddr_aton("de:ad:be:ef:00:11", addr, sizeof(addr));
 
@@ -692,6 +702,42 @@ test_bridge_addr(void)
     g_assert_cmpint(plink->l_address.len, ==, sizeof(addr));
     g_assert(!memcmp(plink->l_address.data, addr, sizeof(addr)));
 
+    info_data = (const NMPlatformLinkSetBridgeInfoData){
+        .vlan_default_pvid_val = nmtst_rand_select(0, 5, 42, 1048),
+        .vlan_default_pvid_has = nmtst_get_rand_bool(),
+        .vlan_filtering_val    = nmtst_get_rand_bool(),
+        .vlan_filtering_has    = nmtst_get_rand_bool(),
+    };
+    b = nm_platform_link_set_bridge_info(NM_PLATFORM_GET, link.ifindex, &info_data);
+    g_assert(b);
+
+    _sysctl_assert_eq(NM_PLATFORM_GET,
+                      "/sys/class/net/" DEVICE_NAME "/bridge/default_pvid",
+                      info_data.vlan_default_pvid_has
+                          ? nm_sprintf_buf(sbuf, "%u", info_data.vlan_default_pvid_val)
+                          : "1");
+
+    _sysctl_assert_eq(NM_PLATFORM_GET,
+                      "/sys/class/net/" DEVICE_NAME "/bridge/vlan_filtering",
+                      info_data.vlan_filtering_val && info_data.vlan_filtering_has ? "1" : "0");
+
+    info_data = (const NMPlatformLinkSetBridgeInfoData){
+        .vlan_default_pvid_val = 55,
+        .vlan_default_pvid_has = TRUE,
+        .vlan_filtering_val    = !info_data.vlan_filtering_val,
+        .vlan_filtering_has    = TRUE,
+    };
+    b = nm_platform_link_set_bridge_info(NM_PLATFORM_GET, link.ifindex, &info_data);
+    g_assert(b);
+
+    _sysctl_assert_eq(NM_PLATFORM_GET,
+                      "/sys/class/net/" DEVICE_NAME "/bridge/default_pvid",
+                      nm_sprintf_buf(sbuf, "%u", info_data.vlan_default_pvid_val));
+
+    _sysctl_assert_eq(NM_PLATFORM_GET,
+                      "/sys/class/net/" DEVICE_NAME "/bridge/vlan_filtering",
+                      info_data.vlan_filtering_val ? "1" : "0");
+
     nmtstp_link_delete(NULL, -1, link.ifindex, link.name, TRUE);
 }
 
@@ -2630,6 +2676,36 @@ test_vlan_set_xgress(void)
 /*****************************************************************************/
 
 static void
+test_link_set_properties(void)
+{
+    const NMPlatformLink     *link;
+    NMPlatformLinkProps       props;
+    NMPlatformLinkChangeFlags flags;
+    int                       ifindex;
+
+    props = (NMPlatformLinkProps){
+        .tx_queue_length  = 599,
+        .gso_max_size     = 10001,
+        .gso_max_segments = 512,
+    };
+    flags = NM_PLATFORM_LINK_CHANGE_TX_QUEUE_LENGTH | NM_PLATFORM_LINK_CHANGE_GSO_MAX_SIZE
+            | NM_PLATFORM_LINK_CHANGE_GSO_MAX_SEGMENTS;
+
+    ifindex = nmtstp_link_dummy_add(NM_PLATFORM_GET, FALSE, "dummy1")->ifindex;
+    g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex, &props, NULL, flags));
+
+    link = nmtstp_link_get(NM_PLATFORM_GET, ifindex, "dummy1");
+    g_assert(link);
+    g_assert_cmpint(link->link_props.tx_queue_length, ==, 599);
+    g_assert_cmpint(link->link_props.gso_max_size, ==, 10001);
+    g_assert_cmpint(link->link_props.gso_max_segments, ==, 512);
+
+    nmtstp_link_delete(NULL, -1, link->ifindex, "dummy1", TRUE);
+}
+
+/*****************************************************************************/
+
+static void
 test_create_many_links_do(guint n_devices)
 {
     gint64                 time, start_time = nm_utils_get_monotonic_timestamp_nsec();
@@ -2985,16 +3061,6 @@ _check_sysctl_skip(void)
 
 /*****************************************************************************/
 
-#define _sysctl_assert_eq(plat, path, value)                                   \
-    G_STMT_START                                                               \
-    {                                                                          \
-        gs_free char *_val = NULL;                                             \
-                                                                               \
-        _val = nm_platform_sysctl_get(plat, NMP_SYSCTL_PATHID_ABSOLUTE(path)); \
-        g_assert_cmpstr(_val, ==, value);                                      \
-    }                                                                          \
-    G_STMT_END
-
 static void
 test_netns_general(gpointer fixture, gconstpointer test_data)
 {
@@ -3533,7 +3599,7 @@ test_sysctl_rename(void)
                         ==,
                         (gint32) nm_platform_sysctl_get_int32(
                             PL,
-                            NMP_SYSCTL_PATHID_NETDIR(dirfd, s ?: "<unknown>", "ifindex"),
+                            NMP_SYSCTL_PATHID_NETDIR_A(dirfd, s ?: "<unknown>", "ifindex"),
                             -1));
         break;
     }
@@ -3605,7 +3671,7 @@ test_sysctl_netns_switch(void)
                     ==,
                     (gint32) nm_platform_sysctl_get_int32(
                         PL,
-                        NMP_SYSCTL_PATHID_NETDIR(dirfd, s ?: "<unknown>", "ifindex"),
+                        NMP_SYSCTL_PATHID_NETDIR_A(dirfd, s ?: "<unknown>", "ifindex"),
                         -1));
     g_assert_cmpint(
         ifindex,
@@ -4022,6 +4088,8 @@ _nmtstp_setup_tests(void)
 
         g_test_add_func("/link/software/vlan/set-xgress", test_vlan_set_xgress);
 
+        g_test_add_func("/link/set-properties", test_link_set_properties);
+
         g_test_add_data_func("/link/create-many-links/20",
                              GUINT_TO_POINTER(20),
                              test_create_many_links);
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index bd8fdc27..9aa21a9a 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -421,7 +421,8 @@ test_ip6_route(void)
                                          in6addr_any,
                                          NM_PLATFORM_LIFETIME_PERMANENT,
                                          NM_PLATFORM_LIFETIME_PERMANENT,
-                                         0));
+                                         0,
+                                         NULL));
     accept_signals(route_added, 0, 3);
 
     _wait_for_ipv6_addr_non_tentative(NM_PLATFORM_GET, 200, ifindex, 1, &pref_src);
@@ -706,7 +707,8 @@ test_ip4_route_options(gconstpointer test_data)
                                              a->lifetime,
                                              a->preferred,
                                              a->n_ifa_flags,
-                                             a->label));
+                                             a->label,
+                                             NULL));
         if (a->peer_address == a->address)
             _wait_for_ipv4_addr_device_route(NM_PLATFORM_GET, 200, a->ifindex, a->address, a->plen);
     }
@@ -878,7 +880,8 @@ test_ip6_route_options(gconstpointer test_data)
                                              addr[i].peer_address,
                                              addr[i].lifetime,
                                              addr[i].preferred,
-                                             addr[i].n_ifa_flags));
+                                             addr[i].n_ifa_flags,
+                                             NULL));
     }
     _wait_for_ipv6_addr_non_tentative(NM_PLATFORM_GET, 400, IFINDEX, addr_n, addr_in6);
 
diff --git a/src/core/settings/nm-secret-agent.c b/src/core/settings/nm-secret-agent.c
index a3df4497..bb300345 100644
--- a/src/core/settings/nm-secret-agent.c
+++ b/src/core/settings/nm-secret-agent.c
@@ -463,7 +463,7 @@ _call_cancel_cb(GObject *source, GAsyncResult *result, gpointer user_data)
  * nm_secret_agent_cancel_call:
  * @self: the #NMSecretAgent instance for the @call_id.
  *   Maybe be %NULL if @call_id is %NULL.
- * @call_id: (allow-none): the call id to cancel. May be %NULL for convenience,
+ * @call_id: (nullable): the call id to cancel. May be %NULL for convenience,
  *   in which case it does nothing.
  *
  * It is an error to pass an invalid @call_id or a @call_id for an operation
diff --git a/src/core/settings/nm-settings-connection.c b/src/core/settings/nm-settings-connection.c
index 024c0009..176cc2c2 100644
--- a/src/core/settings/nm-settings-connection.c
+++ b/src/core/settings/nm-settings-connection.c
@@ -24,13 +24,10 @@
 #include "libnm-core-intern/nm-core-internal.h"
 #include "nm-audit-manager.h"
 #include "nm-settings.h"
+#include "nm-manager.h"
 #include "nm-dbus-manager.h"
 #include "settings/plugins/keyfile/nms-keyfile-storage.h"
 
-#define AUTOCONNECT_RETRIES_UNSET       -2
-#define AUTOCONNECT_RETRIES_FOREVER     -1
-#define AUTOCONNECT_RESET_RETRIES_TIMER 300
-
 #define SEEN_BSSIDS_MAX 30
 
 #define _NM_SETTINGS_UPDATE2_FLAG_ALL_PERSIST_MODES                          \
@@ -112,7 +109,11 @@ _seen_bssids_hash_new(void)
 
 /*****************************************************************************/
 
-NM_GOBJECT_PROPERTIES_DEFINE(NMSettingsConnection, PROP_UNSAVED, PROP_FLAGS, PROP_FILENAME, );
+NM_GOBJECT_PROPERTIES_DEFINE(NMSettingsConnection,
+                             PROP_VERSION_ID,
+                             PROP_UNSAVED,
+                             PROP_FLAGS,
+                             PROP_FILENAME, );
 
 enum { UPDATED_INTERNAL, FLAGS_CHANGED, LAST_SIGNAL };
 
@@ -159,9 +160,7 @@ typedef struct _NMSettingsConnectionPrivate {
 
     guint64 last_secret_agent_version_id;
 
-    int autoconnect_retries;
-
-    gint32 autoconnect_retries_blocked_until;
+    guint64 version_id;
 
     bool timestamp_set : 1;
 
@@ -227,6 +226,22 @@ static guint _get_seen_bssids(NMSettingsConnection *self,
 
 /*****************************************************************************/
 
+NMSettings *
+nm_settings_connection_get_settings(NMSettingsConnection *self)
+{
+    g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(self), NULL);
+
+    return NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->settings;
+}
+
+NMManager *
+nm_settings_connection_get_manager(NMSettingsConnection *self)
+{
+    return nm_settings_get_manager(nm_settings_connection_get_settings(self));
+}
+
+/*****************************************************************************/
+
 NMDevice *
 nm_settings_connection_default_wired_get_device(NMSettingsConnection *self)
 {
@@ -361,6 +376,20 @@ nm_settings_connection_get_connection(NMSettingsConnection *self)
     return NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->connection;
 }
 
+gpointer
+nm_settings_connection_get_setting(NMSettingsConnection *self, NMMetaSettingType meta_type)
+{
+    NMConnection *connection;
+
+    nm_assert(NM_IS_SETTINGS_CONNECTION(self));
+
+    connection = NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->connection;
+
+    nm_assert(NM_IS_SIMPLE_CONNECTION(connection));
+
+    return _nm_connection_get_setting_by_metatype_unsafe(connection, meta_type);
+}
+
 void
 _nm_settings_connection_set_connection(NMSettingsConnection            *self,
                                        NMConnection                    *new_connection,
@@ -1049,7 +1078,7 @@ get_secrets_idle_cb(NMSettingsConnectionCallId *call_id)
 /**
  * nm_settings_connection_get_secrets:
  * @self: the #NMSettingsConnection
- * @applied_connection: (allow-none): if provided, only request secrets
+ * @applied_connection: (nullable): if provided, only request secrets
  *   if @self equals to @applied_connection. Also, update the secrets
  *   in the @applied_connection.
  * @subject: the #NMAuthSubject originating the request
@@ -1414,6 +1443,7 @@ typedef struct {
     NMSettingsUpdate2Flags flags;
     char                  *audit_args;
     char                  *plugin_name;
+    guint64                version_id;
     bool                   is_update2 : 1;
 } UpdateInfo;
 
@@ -1442,53 +1472,7 @@ update_complete(NMSettingsConnection *self, UpdateInfo *info, GError *error)
     g_clear_object(&info->new_settings);
     g_free(info->audit_args);
     g_free(info->plugin_name);
-    g_slice_free(UpdateInfo, info);
-}
-
-static int
-_autoconnect_retries_initial(NMSettingsConnection *self)
-{
-    NMSettingConnection *s_con;
-    int                  retries = -1;
-
-    s_con = nm_connection_get_setting_connection(nm_settings_connection_get_connection(self));
-    if (s_con)
-        retries = nm_setting_connection_get_autoconnect_retries(s_con);
-
-    /* -1 means 'default' */
-    if (retries == -1)
-        retries = nm_config_data_get_autoconnect_retries_default(NM_CONFIG_GET_DATA);
-
-    /* 0 means 'forever', which is translated to a retry count of -1 */
-    if (retries == 0)
-        retries = AUTOCONNECT_RETRIES_FOREVER;
-
-    nm_assert(retries == AUTOCONNECT_RETRIES_FOREVER || retries >= 0);
-    return retries;
-}
-
-static void
-_autoconnect_retries_set(NMSettingsConnection *self, int retries, gboolean is_reset)
-{
-    NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
-
-    g_return_if_fail(retries == AUTOCONNECT_RETRIES_FOREVER || retries >= 0);
-
-    if (priv->autoconnect_retries != retries) {
-        _LOGT("autoconnect: retries set %d%s", retries, is_reset ? " (reset)" : "");
-        priv->autoconnect_retries = retries;
-    }
-
-    if (retries)
-        priv->autoconnect_retries_blocked_until = 0;
-    else {
-        /* NOTE: the blocked time must be identical for all connections, otherwise
-         * the tracking of resetting the retry count in NMPolicy needs adjustment
-         * in _connection_autoconnect_retries_set() (as it would need to re-evaluate
-         * the next-timeout every time a connection gets blocked). */
-        priv->autoconnect_retries_blocked_until =
-            nm_utils_get_monotonic_timestamp_sec() + AUTOCONNECT_RESET_RETRIES_TIMER;
-    }
+    nm_g_slice_free(info);
 }
 
 static void
@@ -1502,14 +1486,23 @@ update_auth_cb(NMSettingsConnection  *self,
     UpdateInfo                     *info  = data;
     gs_free_error GError           *local = NULL;
     NMSettingsConnectionPersistMode persist_mode;
+    gs_unref_object NMConnection   *for_agent = NULL;
 
-    if (error) {
-        update_complete(self, info, error);
-        return;
-    }
+    if (error)
+        goto out;
 
     priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
 
+    if (info->version_id != 0 && info->version_id != priv->version_id) {
+        g_set_error_literal(&local,
+                            NM_SETTINGS_ERROR,
+                            NM_SETTINGS_ERROR_VERSION_ID_MISMATCH,
+                            "Update failed because profile changed in the meantime and the "
+                            "version-id mismatches");
+        error = local;
+        goto out;
+    }
+
     if (info->new_settings) {
         if (!_nm_connection_aggregate(info->new_settings,
                                       NM_CONNECTION_AGGREGATE_ANY_SECRETS,
@@ -1539,10 +1532,13 @@ update_auth_cb(NMSettingsConnection  *self,
             /* New secrets, allow autoconnection again */
             if (nm_settings_connection_autoconnect_blocked_reason_set(
                     self,
-                    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS,
+                    NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS,
                     FALSE)
                 && !nm_settings_connection_autoconnect_blocked_reason_get(self))
-                nm_settings_connection_autoconnect_retries_reset(self);
+                nm_manager_devcon_autoconnect_retries_reset(
+                    nm_settings_connection_get_manager(self),
+                    NULL,
+                    self);
         }
     }
 
@@ -1586,10 +1582,9 @@ update_auth_cb(NMSettingsConnection  *self,
              : NM_SETTINGS_CONNECTION_INT_FLAGS_NONE),
         NM_SETTINGS_CONNECTION_INT_FLAGS_NM_GENERATED | NM_SETTINGS_CONNECTION_INT_FLAGS_VOLATILE
             | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL,
-        NM_SETTINGS_CONNECTION_UPDATE_REASON_FORCE_RENAME
-            | (NM_FLAGS_HAS(info->flags, NM_SETTINGS_UPDATE2_FLAG_NO_REAPPLY)
-                   ? NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE
-                   : NM_SETTINGS_CONNECTION_UPDATE_REASON_REAPPLY_PARTIAL)
+        (NM_FLAGS_HAS(info->flags, NM_SETTINGS_UPDATE2_FLAG_NO_REAPPLY)
+             ? NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE
+             : NM_SETTINGS_CONNECTION_UPDATE_REASON_REAPPLY_PARTIAL)
             | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_SYSTEM_SECRETS
             | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_AGENT_SECRETS
             | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET
@@ -1599,25 +1594,29 @@ update_auth_cb(NMSettingsConnection  *self,
         "update-from-dbus",
         &local);
 
-    if (!local) {
-        gs_unref_object NMConnection *for_agent = NULL;
-
-        /* Dupe the connection so we can clear out non-agent-owned secrets,
-         * as agent-owned secrets are the only ones we send back to be saved.
-         * Only send secrets to agents of the same UID that called update too.
-         */
-        for_agent = nm_simple_connection_new_clone(nm_settings_connection_get_connection(self));
-        _nm_connection_clear_secrets_by_secret_flags(for_agent, NM_SETTING_SECRET_FLAG_AGENT_OWNED);
-        nm_agent_manager_save_secrets(info->agent_mgr,
-                                      nm_dbus_object_get_path(NM_DBUS_OBJECT(self)),
-                                      for_agent,
-                                      info->subject);
+    if (local) {
+        error = local;
+        goto out;
     }
 
+    /* Dupe the connection so we can clear out non-agent-owned secrets,
+     * as agent-owned secrets are the only ones we send back to be saved.
+     * Only send secrets to agents of the same UID that called update too.
+     */
+    for_agent = nm_simple_connection_new_clone(nm_settings_connection_get_connection(self));
+    _nm_connection_clear_secrets_by_secret_flags(for_agent, NM_SETTING_SECRET_FLAG_AGENT_OWNED);
+    nm_agent_manager_save_secrets(info->agent_mgr,
+                                  nm_dbus_object_get_path(NM_DBUS_OBJECT(self)),
+                                  for_agent,
+                                  info->subject);
+
     /* Reset auto retries back to default since connection was updated */
-    nm_settings_connection_autoconnect_retries_reset(self);
+    nm_manager_devcon_autoconnect_retries_reset(nm_settings_connection_get_manager(self),
+                                                NULL,
+                                                self);
 
-    update_complete(self, info, local);
+out:
+    update_complete(self, info, error);
 }
 
 static const char *
@@ -1650,6 +1649,7 @@ settings_connection_update(NMSettingsConnection  *self,
                            GDBusMethodInvocation *context,
                            GVariant              *new_settings,
                            const char            *plugin_name,
+                           guint64                version_id,
                            NMSettingsUpdate2Flags flags)
 {
     NMSettingsConnectionPrivate *priv    = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
@@ -1697,14 +1697,17 @@ settings_connection_update(NMSettingsConnection  *self,
                                              &error))
         goto error;
 
-    info               = g_slice_new0(UpdateInfo);
-    info->is_update2   = is_update2;
-    info->context      = context;
-    info->agent_mgr    = g_object_ref(priv->agent_mgr);
-    info->subject      = subject;
-    info->flags        = flags;
-    info->new_settings = tmp;
-    info->plugin_name  = g_strdup(plugin_name);
+    info  = g_slice_new(UpdateInfo);
+    *info = (UpdateInfo){
+        .is_update2   = is_update2,
+        .context      = context,
+        .agent_mgr    = g_object_ref(priv->agent_mgr),
+        .subject      = subject,
+        .flags        = flags,
+        .new_settings = tmp,
+        .plugin_name  = g_strdup(plugin_name),
+        .version_id   = version_id,
+    };
 
     permission = get_update_modify_permission(nm_settings_connection_get_connection(self),
                                               tmp ?: nm_settings_connection_get_connection(self));
@@ -1738,6 +1741,7 @@ impl_settings_connection_update(NMDBusObject                      *obj,
                                invocation,
                                settings,
                                NULL,
+                               0,
                                NM_SETTINGS_UPDATE2_FLAG_TO_DISK);
 }
 
@@ -1759,6 +1763,7 @@ impl_settings_connection_update_unsaved(NMDBusObject                      *obj,
                                invocation,
                                settings,
                                NULL,
+                               0,
                                NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY);
 }
 
@@ -1778,6 +1783,7 @@ impl_settings_connection_save(NMDBusObject                      *obj,
                                invocation,
                                NULL,
                                NULL,
+                               0,
                                NM_SETTINGS_UPDATE2_FLAG_TO_DISK);
 }
 
@@ -1794,6 +1800,7 @@ impl_settings_connection_update2(NMDBusObject                      *obj,
     gs_unref_variant GVariant *settings    = NULL;
     gs_unref_variant GVariant *args        = NULL;
     gs_free char              *plugin_name = NULL;
+    guint64                    version_id  = 0;
     guint32                    flags_u;
     GError                    *error = NULL;
     GVariantIter               iter;
@@ -1840,6 +1847,11 @@ impl_settings_connection_update2(NMDBusObject                      *obj,
             plugin_name = g_variant_dup_string(args_value, NULL);
             continue;
         }
+        if (nm_streq(args_name, "version-id")
+            && g_variant_is_of_type(args_value, G_VARIANT_TYPE_UINT64)) {
+            version_id = g_variant_get_uint64(args_value);
+            continue;
+        }
 
         error = g_error_new(NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_ARGUMENTS,
@@ -1849,7 +1861,7 @@ impl_settings_connection_update2(NMDBusObject                      *obj,
         return;
     }
 
-    settings_connection_update(self, TRUE, invocation, settings, plugin_name, flags);
+    settings_connection_update(self, TRUE, invocation, settings, plugin_name, version_id, flags);
 }
 
 static void
@@ -2542,56 +2554,6 @@ nm_settings_connection_get_num_seen_bssids(NMSettingsConnection *self)
 
 /*****************************************************************************/
 
-/**
- * nm_settings_connection_autoconnect_retries_get:
- * @self: the settings connection
- *
- * Returns the number of autoconnect retries left. If the value is
- * not yet set, initialize it with the value from the connection or
- * with the global default.
- */
-int
-nm_settings_connection_autoconnect_retries_get(NMSettingsConnection *self)
-{
-    NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
-
-    if (G_UNLIKELY(priv->autoconnect_retries == AUTOCONNECT_RETRIES_UNSET)) {
-        _autoconnect_retries_set(self, _autoconnect_retries_initial(self), TRUE);
-    }
-    return priv->autoconnect_retries;
-}
-
-void
-nm_settings_connection_autoconnect_retries_set(NMSettingsConnection *self, int retries)
-{
-    g_return_if_fail(NM_IS_SETTINGS_CONNECTION(self));
-    g_return_if_fail(retries >= 0);
-
-    _autoconnect_retries_set(self, retries, FALSE);
-}
-
-void
-nm_settings_connection_autoconnect_retries_reset(NMSettingsConnection *self)
-{
-    g_return_if_fail(NM_IS_SETTINGS_CONNECTION(self));
-
-    _autoconnect_retries_set(self, _autoconnect_retries_initial(self), TRUE);
-}
-
-gint32
-nm_settings_connection_autoconnect_retries_blocked_until(NMSettingsConnection *self)
-{
-    return NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->autoconnect_retries_blocked_until;
-}
-
-static NM_UTILS_FLAGS2STR_DEFINE(
-    _autoconnect_blocked_reason_to_string,
-    NMSettingsAutoconnectBlockedReason,
-    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE, "none"),
-    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST, "user-request"),
-    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED, "failed"),
-    NM_UTILS_FLAGS2STR(NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS, "no-secrets"), );
-
 NMSettingsAutoconnectBlockedReason
 nm_settings_connection_autoconnect_blocked_reason_get(NMSettingsConnection *self)
 {
@@ -2599,25 +2561,41 @@ nm_settings_connection_autoconnect_blocked_reason_get(NMSettingsConnection *self
 }
 
 gboolean
-nm_settings_connection_autoconnect_blocked_reason_set_full(NMSettingsConnection              *self,
-                                                           NMSettingsAutoconnectBlockedReason mask,
-                                                           NMSettingsAutoconnectBlockedReason value)
+nm_settings_connection_autoconnect_blocked_reason_set(NMSettingsConnection              *self,
+                                                      NMSettingsAutoconnectBlockedReason reason,
+                                                      gboolean                           set)
 {
     NMSettingsAutoconnectBlockedReason v;
     NMSettingsConnectionPrivate       *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
-    char                               buf[100];
+    char                               buf1[200];
+    char                               buf2[200];
 
-    nm_assert(mask);
-    nm_assert(!NM_FLAGS_ANY(value, ~mask));
+    nm_assert(reason != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE);
+    nm_assert(!NM_FLAGS_ANY(reason,
+                            ~(NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST
+                              | NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NO_SECRETS)));
 
     v = priv->autoconnect_blocked_reason;
-    v = (v & ~mask) | (value & mask);
+    v = NM_FLAGS_ASSIGN(v, reason, set);
 
     if (priv->autoconnect_blocked_reason == v)
         return FALSE;
 
-    _LOGT("autoconnect: blocked reason: %s",
-          _autoconnect_blocked_reason_to_string(v, buf, sizeof(buf)));
+    if (set) {
+        _LOGT("block-autoconnect: profile: blocked with reason %s (%s %s)",
+              nm_settings_autoconnect_blocked_reason_to_string(v, buf1, sizeof(buf1)),
+              "just blocked",
+              nm_settings_autoconnect_blocked_reason_to_string(reason, buf2, sizeof(buf2)));
+    } else if (v != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE) {
+        _LOGT("block-autoconnect: profile: blocked with reason %s (%s %s)",
+              nm_settings_autoconnect_blocked_reason_to_string(v, buf1, sizeof(buf1)),
+              "just unblocked",
+              nm_settings_autoconnect_blocked_reason_to_string(reason, buf2, sizeof(buf2)));
+    } else {
+        _LOGT("block-autoconnect: profile: not blocked (unblocked %s)",
+              nm_settings_autoconnect_blocked_reason_to_string(reason, buf1, sizeof(buf1)));
+    }
+
     priv->autoconnect_blocked_reason = v;
     return TRUE;
 }
@@ -2632,9 +2610,7 @@ nm_settings_connection_autoconnect_is_blocked(NMSettingsConnection *self)
 
     priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
 
-    if (priv->autoconnect_blocked_reason != NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE)
-        return TRUE;
-    if (priv->autoconnect_retries == 0)
+    if (priv->autoconnect_blocked_reason != NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_NONE)
         return TRUE;
 
     flags = priv->flags;
@@ -2693,6 +2669,23 @@ nm_settings_connection_get_uuid(NMSettingsConnection *self)
     return uuid;
 }
 
+guint64
+nm_settings_connection_get_version_id(NMSettingsConnection *self)
+{
+    g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(self), 0);
+
+    return NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->version_id;
+}
+
+void
+nm_settings_connection_bump_version_id(NMSettingsConnection *self)
+{
+    g_return_if_fail(NM_IS_SETTINGS_CONNECTION(self));
+
+    NM_SETTINGS_CONNECTION_GET_PRIVATE(self)->version_id++;
+    _notify(self, PROP_VERSION_ID);
+}
+
 const char *
 nm_settings_connection_get_connection_type(NMSettingsConnection *self)
 {
@@ -2716,9 +2709,13 @@ _nm_settings_connection_cleanup_after_remove(NMSettingsConnection *self)
 static void
 get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
-    NMSettingsConnection *self = NM_SETTINGS_CONNECTION(object);
+    NMSettingsConnection        *self = NM_SETTINGS_CONNECTION(object);
+    NMSettingsConnectionPrivate *priv = NM_SETTINGS_CONNECTION_GET_PRIVATE(self);
 
     switch (prop_id) {
+    case PROP_VERSION_ID:
+        g_value_set_uint64(value, priv->version_id);
+        break;
     case PROP_UNSAVED:
         g_value_set_boolean(value, nm_settings_connection_get_unsaved(self));
         break;
@@ -2748,6 +2745,7 @@ nm_settings_connection_init(NMSettingsConnection *self)
     self->_priv = priv;
 
     c_list_init(&self->_connections_lst);
+    c_list_init(&self->devcon_con_lst_head);
     c_list_init(&priv->seen_bssids_lst_head);
     c_list_init(&priv->call_ids_lst_head);
     c_list_init(&priv->auth_lst_head);
@@ -2755,7 +2753,7 @@ nm_settings_connection_init(NMSettingsConnection *self)
     priv->agent_mgr = g_object_ref(nm_agent_manager_get());
     priv->settings  = g_object_ref(nm_settings_get());
 
-    priv->autoconnect_retries = AUTOCONNECT_RETRIES_UNSET;
+    priv->version_id = 1;
 }
 
 NMSettingsConnection *
@@ -2776,6 +2774,7 @@ dispose(GObject *object)
     nm_assert(!priv->default_wired_device);
 
     nm_assert(c_list_is_empty(&self->_connections_lst));
+    nm_assert(c_list_is_empty(&self->devcon_con_lst_head));
     nm_assert(c_list_is_empty(&priv->auth_lst_head));
 
     /* Cancel in-progress secrets requests */
@@ -2868,7 +2867,10 @@ static const NMDBusInterfaceInfoExtended interface_info_settings_connection = {
                                                            NM_SETTINGS_CONNECTION_FLAGS),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Filename",
                                                            "s",
-                                                           NM_SETTINGS_CONNECTION_FILENAME), ), ),
+                                                           NM_SETTINGS_CONNECTION_FILENAME),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("VersionId",
+                                                           "t",
+                                                           NM_SETTINGS_CONNECTION_VERSION_ID), ), ),
 };
 
 static void
@@ -2886,6 +2888,15 @@ nm_settings_connection_class_init(NMSettingsConnectionClass *klass)
     object_class->dispose      = dispose;
     object_class->get_property = get_property;
 
+    obj_properties[PROP_VERSION_ID] =
+        g_param_spec_uint64(NM_SETTINGS_CONNECTION_VERSION_ID,
+                            "",
+                            "",
+                            0,
+                            G_MAXUINT64,
+                            0,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
     obj_properties[PROP_UNSAVED] = g_param_spec_boolean(NM_SETTINGS_CONNECTION_UNSAVED,
                                                         "",
                                                         "",
diff --git a/src/core/settings/nm-settings-connection.h b/src/core/settings/nm-settings-connection.h
index fce7497c..835a978e 100644
--- a/src/core/settings/nm-settings-connection.h
+++ b/src/core/settings/nm-settings-connection.h
@@ -7,8 +7,11 @@
 #ifndef __NETWORKMANAGER_SETTINGS_CONNECTION_H__
 #define __NETWORKMANAGER_SETTINGS_CONNECTION_H__
 
+#include "libnm-core-intern/nm-meta-setting-base.h"
+
 #include "nm-dbus-object.h"
 #include "nm-connection.h"
+#include "NetworkManagerUtils.h"
 
 #include "nm-settings-storage.h"
 
@@ -138,9 +141,10 @@ typedef enum {
 #define NM_SETTINGS_CONNECTION_FLAGS_CHANGED    "flags-changed"
 
 /* Properties */
-#define NM_SETTINGS_CONNECTION_UNSAVED  "unsaved"
-#define NM_SETTINGS_CONNECTION_FLAGS    "flags"
-#define NM_SETTINGS_CONNECTION_FILENAME "filename"
+#define NM_SETTINGS_CONNECTION_UNSAVED    "unsaved"
+#define NM_SETTINGS_CONNECTION_VERSION_ID "version-id"
+#define NM_SETTINGS_CONNECTION_FLAGS      "flags"
+#define NM_SETTINGS_CONNECTION_FILENAME   "filename"
 
 /**
  * NMSettingsConnectionIntFlags:
@@ -188,19 +192,6 @@ typedef enum _NMSettingsConnectionIntFlags {
     _NM_SETTINGS_CONNECTION_INT_FLAGS_ALL = ((_NM_SETTINGS_CONNECTION_INT_FLAGS_LAST - 1) << 1) - 1,
 } NMSettingsConnectionIntFlags;
 
-typedef enum {
-    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE = 0,
-
-    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST = (1LL << 0),
-    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED       = (1LL << 1),
-    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS   = (1LL << 2),
-
-    NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_ALL =
-        (NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST
-         | NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_FAILED
-         | NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NO_SECRETS),
-} NMSettingsAutoconnectBlockedReason;
-
 typedef struct _NMSettingsConnectionCallId NMSettingsConnectionCallId;
 
 typedef struct _NMSettingsConnectionClass NMSettingsConnectionClass;
@@ -210,6 +201,7 @@ struct _NMSettingsConnectionPrivate;
 struct _NMSettingsConnection {
     NMDBusObject                         parent;
     CList                                _connections_lst;
+    CList                                devcon_con_lst_head;
     struct _NMSettingsConnectionPrivate *_priv;
 };
 
@@ -217,7 +209,13 @@ GType nm_settings_connection_get_type(void);
 
 NMSettingsConnection *nm_settings_connection_new(void);
 
+NMSettings *nm_settings_connection_get_settings(NMSettingsConnection *self);
+
+NMManager *nm_settings_connection_get_manager(NMSettingsConnection *self);
+
 NMConnection *nm_settings_connection_get_connection(NMSettingsConnection *self);
+gpointer      nm_settings_connection_get_setting(NMSettingsConnection *self,
+                                                 NMMetaSettingType     meta_type);
 
 void _nm_settings_connection_set_connection(NMSettingsConnection            *self,
                                             NMConnection                    *new_connection,
@@ -234,6 +232,9 @@ const char *nm_settings_connection_get_filename(NMSettingsConnection *self);
 
 guint64 nm_settings_connection_get_last_secret_agent_version_id(NMSettingsConnection *self);
 
+guint64 nm_settings_connection_get_version_id(NMSettingsConnection *self);
+void    nm_settings_connection_bump_version_id(NMSettingsConnection *self);
+
 gboolean
 nm_settings_connection_has_unmodified_applied_connection(NMSettingsConnection *self,
                                                          NMConnection         *applied_connection,
@@ -347,31 +348,15 @@ void nm_settings_connection_add_seen_bssid(NMSettingsConnection *self, const cha
 
 guint nm_settings_connection_get_num_seen_bssids(NMSettingsConnection *self);
 
-int  nm_settings_connection_autoconnect_retries_get(NMSettingsConnection *self);
-void nm_settings_connection_autoconnect_retries_set(NMSettingsConnection *self, int retries);
-void nm_settings_connection_autoconnect_retries_reset(NMSettingsConnection *self);
-
-gint32 nm_settings_connection_autoconnect_retries_blocked_until(NMSettingsConnection *self);
+gboolean nm_settings_connection_autoconnect_is_blocked(NMSettingsConnection *self);
 
 NMSettingsAutoconnectBlockedReason
-         nm_settings_connection_autoconnect_blocked_reason_get(NMSettingsConnection *self);
-gboolean nm_settings_connection_autoconnect_blocked_reason_set_full(
-    NMSettingsConnection              *self,
-    NMSettingsAutoconnectBlockedReason mask,
-    NMSettingsAutoconnectBlockedReason value);
+nm_settings_connection_autoconnect_blocked_reason_get(NMSettingsConnection *self);
 
-static inline gboolean
+gboolean
 nm_settings_connection_autoconnect_blocked_reason_set(NMSettingsConnection              *self,
-                                                      NMSettingsAutoconnectBlockedReason mask,
-                                                      gboolean                           set)
-{
-    return nm_settings_connection_autoconnect_blocked_reason_set_full(
-        self,
-        mask,
-        set ? mask : NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_NONE);
-}
-
-gboolean nm_settings_connection_autoconnect_is_blocked(NMSettingsConnection *self);
+                                                      NMSettingsAutoconnectBlockedReason reason,
+                                                      gboolean                           set);
 
 const char *nm_settings_connection_get_id(NMSettingsConnection *connection);
 const char *nm_settings_connection_get_uuid(NMSettingsConnection *connection);
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index 9995b490..8796de36 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -222,7 +222,7 @@ _sett_conn_entry_get_conn(SettConnEntry *sett_conn_entry)
  * _sett_conn_entry_storage_find_conflicting_storage:
  * @sett_conn_entry: the list of settings-storages for the given UUID.
  * @target_plugin: the settings plugin to check
- * @storage_check_including: (allow-none): optionally compare against this storage.
+ * @storage_check_including: (nullable): optionally compare against this storage.
  * @plugins: the list of plugins sorted in descending priority. This determines
  *   the priority and whether a storage conflicts.
  *
@@ -451,6 +451,16 @@ static void _startup_complete_check(NMSettings *self, gint64 now_msec);
 
 /*****************************************************************************/
 
+NMManager *
+nm_settings_get_manager(NMSettings *self)
+{
+    g_return_val_if_fail(NM_IS_SETTINGS(self), NULL);
+
+    return NM_SETTINGS_GET_PRIVATE(self)->manager;
+}
+
+/*****************************************************************************/
+
 static void
 _emit_connection_added(NMSettings *self, NMSettingsConnection *sett_conn)
 {
@@ -525,7 +535,7 @@ _startup_complete_check_is_ready(NMSettings           *self,
         /* Check that device is compatible with the device. We are also happy
          * with a device compatible but for which the connection is disallowed
          * by NM configuration. */
-        if (!nm_device_check_connection_compatible(device, conn, &error)
+        if (!nm_device_check_connection_compatible(device, conn, TRUE, &error)
             && !g_error_matches(error,
                                 NM_UTILS_ERROR,
                                 NM_UTILS_ERROR_CONNECTION_AVAILABLE_DISALLOWED))
@@ -1080,11 +1090,13 @@ _connection_changed_update(NMSettings                      *self,
 
     is_new = c_list_is_empty(&sett_conn->_connections_lst);
 
-    _LOGT("update[%s]: %s connection \"%s\" (" NM_SETTINGS_STORAGE_PRINT_FMT ")",
+    _LOGT("update[%s]: %s connection \"%s\" (" NM_SETTINGS_STORAGE_PRINT_FMT "), "
+          "new version-id %" G_GUINT64_FORMAT,
           nm_settings_storage_get_uuid(storage),
           is_new ? "adding" : "updating",
           nm_connection_get_id(connection),
-          NM_SETTINGS_STORAGE_PRINT_ARG(storage));
+          NM_SETTINGS_STORAGE_PRINT_ARG(storage),
+          (nm_settings_connection_get_version_id(sett_conn) + 1u));
 
     _nm_settings_connection_set_storage(sett_conn, storage);
 
@@ -1109,7 +1121,7 @@ _connection_changed_update(NMSettings                      *self,
     if (NM_FLAGS_HAS(update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_BLOCK_AUTOCONNECT)) {
         nm_settings_connection_autoconnect_blocked_reason_set(
             sett_conn,
-            NM_SETTINGS_AUTO_CONNECT_BLOCKED_REASON_USER_REQUEST,
+            NM_SETTINGS_AUTOCONNECT_BLOCKED_REASON_USER_REQUEST,
             TRUE);
     }
 
@@ -1156,6 +1168,8 @@ _connection_changed_update(NMSettings                      *self,
                                      path);
     }
 
+    nm_settings_connection_bump_version_id(sett_conn);
+
     if (is_new) {
         nm_dbus_object_emit_signal(NM_DBUS_OBJECT(self),
                                    &interface_info_settings,
@@ -1237,6 +1251,8 @@ _connection_changed_delete(NMSettings           *self,
                                          | NM_SETTINGS_CONNECTION_INT_FLAGS_EXTERNAL,
                                      FALSE);
 
+    nm_manager_notify_delete_settings_connections(priv->manager, sett_conn);
+
     _emit_connection_removed(self, sett_conn);
 
     _nm_settings_connection_cleanup_after_remove(sett_conn);
@@ -1453,10 +1469,16 @@ static void
 _plugin_connections_reload(NMSettings *self)
 {
     NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
-    GSList            *iter;
-
-    for (iter = priv->plugins; iter; iter = iter->next) {
-        nm_settings_plugin_reload_connections(iter->data, _plugin_connections_reload_cb, self);
+    GSList            *iter_plugin;
+    GHashTableIter     iter_entry;
+    SettConnEntry     *entry;
+    gboolean           warned = FALSE;
+    gboolean           migrate;
+
+    for (iter_plugin = priv->plugins; iter_plugin; iter_plugin = iter_plugin->next) {
+        nm_settings_plugin_reload_connections(iter_plugin->data,
+                                              _plugin_connections_reload_cb,
+                                              self);
     }
 
     _connection_changed_process_all_dirty(
@@ -1469,8 +1491,53 @@ _plugin_connections_reload(NMSettings *self)
             | NM_SETTINGS_CONNECTION_UPDATE_REASON_RESET_AGENT_SECRETS
             | NM_SETTINGS_CONNECTION_UPDATE_REASON_UPDATE_NON_SECRET);
 
-    for (iter = priv->plugins; iter; iter = iter->next)
-        nm_settings_plugin_load_connections_done(iter->data);
+    for (iter_plugin = priv->plugins; iter_plugin; iter_plugin = iter_plugin->next)
+        nm_settings_plugin_load_connections_done(iter_plugin->data);
+
+    migrate = nm_config_data_get_value_boolean(nm_config_get_data(priv->config),
+                                               NM_CONFIG_KEYFILE_GROUP_MAIN,
+                                               NM_CONFIG_KEYFILE_KEY_MAIN_MIGRATE_IFCFG_RH,
+                                               NM_CONFIG_DEFAULT_MAIN_MIGRATE_IFCFG_RH_BOOL);
+
+    g_hash_table_iter_init(&iter_entry, priv->sce_idx);
+    while (g_hash_table_iter_next(&iter_entry, (gpointer *) &entry, NULL)) {
+        const char *plugin;
+
+        plugin = nm_settings_plugin_get_plugin_name(nm_settings_storage_get_plugin(entry->storage));
+
+        if (nm_streq0(plugin, "ifcfg-rh")) {
+            if (!warned) {
+                if (migrate) {
+                    nm_log_warn(
+                        LOGD_SETTINGS,
+                        "Warning: connections were found in ifcfg-rh format and the "
+                        "\"main.migrate-ifcfg-rh\" option is enabled. Those connections will be "
+                        "migrated to keyfile. To convert them back, disable the option and then "
+                        "run \"nmcli connection migrate --plugin ifcfg-rh $UUID\"");
+                } else {
+                    nm_log_info(
+                        LOGD_SETTINGS,
+                        "Warning: the ifcfg-rh plugin is deprecated, please migrate connections "
+                        "to the keyfile format using \"nmcli connection migrate\"");
+                }
+                warned = TRUE;
+            }
+            if (migrate) {
+                _LOGW("migrating connection %s ('%s') from ifcfg-rh to keyfile",
+                      entry->uuid,
+                      nm_settings_connection_get_id(entry->sett_conn));
+                nm_settings_connection_update(entry->sett_conn,
+                                              "keyfile",
+                                              NULL,
+                                              NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP,
+                                              NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                              NM_SETTINGS_CONNECTION_INT_FLAGS_NONE,
+                                              NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE,
+                                              "migrate-ifcfg-rh",
+                                              NULL);
+            }
+        }
+    }
 }
 
 /*****************************************************************************/
@@ -1728,7 +1795,8 @@ _set_nmmeta_tombstone(NMSettings *self,
  * @persist_mode: the persist-mode for this profile.
  * @add_reason: the add-reason flags.
  * @sett_flags: the settings flags to set.
- * @out_sett_conn: (allow-none) (transfer none): the added settings connection on success.
+ * @out_sett_conn: (out) (optional) (nullable) (transfer none): the added
+ *   settings connection on success.
  * @error: on return, a location to store any errors that may occur
  *
  * Creates a new #NMSettingsConnection for the given source @connection.
@@ -2000,9 +2068,10 @@ nm_settings_update_connection(NMSettings                      *self,
     gs_unref_object NMConnection      *new_connection_cloned = NULL;
     gs_unref_object NMConnection      *new_connection        = NULL;
     NMConnection                      *new_connection_real;
-    gs_unref_object NMSettingsStorage *cur_storage  = NULL;
-    gs_unref_object NMSettingsStorage *new_storage  = NULL;
-    NMSettingsStorage                 *drop_storage = NULL;
+    gs_unref_object NMSettingsStorage *cur_storage         = NULL;
+    gs_unref_object NMSettingsStorage *new_storage         = NULL;
+    NMSettingsStorage                 *drop_storage        = NULL;
+    NMSettingsStorage                 *prev_update_storage = NULL;
     SettConnEntry                     *sett_conn_entry;
     gboolean                           cur_in_memory;
     gboolean                           new_in_memory;
@@ -2246,16 +2315,17 @@ nm_settings_update_connection(NMSettings                      *self,
                                                       drop_storage,
                                                       &local);
         } else {
-            success = _update_connection_to_plugin(self,
-                                                   update_storage,
-                                                   connection,
-                                                   new_flags,
-                                                   update_reason,
-                                                   new_shadowed_storage_filename,
-                                                   new_shadowed_owned,
-                                                   &new_storage,
-                                                   &new_connection,
-                                                   &local);
+            success = _update_connection_to_plugin(
+                self,
+                update_storage,
+                connection,
+                new_flags,
+                NM_FLAGS_HAS(update_reason, NM_SETTINGS_CONNECTION_UPDATE_REASON_FORCE_RENAME),
+                new_shadowed_storage_filename,
+                new_shadowed_owned,
+                &new_storage,
+                &new_connection,
+                &local);
         }
         if (!success) {
             gboolean ignore_failure;
@@ -2306,6 +2376,9 @@ nm_settings_update_connection(NMSettings                      *self,
                 nm_assert_not_reached();
                 new_connection_real = new_connection;
             }
+
+            if (update_storage && new_storage != update_storage)
+                prev_update_storage = update_storage;
         }
     }
 
@@ -2314,6 +2387,12 @@ nm_settings_update_connection(NMSettings                      *self,
 
     _connection_changed_track(self, new_storage, new_connection_real, TRUE);
 
+    if (prev_update_storage) {
+        /* The storage was swapped by the update call. The old one needs
+         * to be dropped, which we do by setting the connection to NULL. */
+        _connection_changed_track(self, prev_update_storage, NULL, FALSE);
+    }
+
     if (drop_storage && drop_storage != new_storage) {
         gs_free_error GError *local = NULL;
 
@@ -3102,7 +3181,7 @@ error:
 /**
  * nm_settings_get_connections:
  * @self: the #NMSettings
- * @out_len: (out) (allow-none): returns the number of returned
+ * @out_len: (out) (optional): returns the number of returned
  *   connections.
  *
  * Returns: (transfer none): a list of NMSettingsConnections. The list is
@@ -3205,10 +3284,10 @@ nm_settings_get_connections_sorted_by_autoconnect_priority(NMSettings *self, gui
 /**
  * nm_settings_get_connections_clone:
  * @self: the #NMSetting
- * @out_len: (allow-none): optional output argument
+ * @out_len: (optional): optional output argument
  * @func: caller-supplied function for filtering connections
  * @func_data: caller-supplied data passed to @func
- * @sort_compare_func: (allow-none): optional function pointer for
+ * @sort_compare_func: (nullable): optional function pointer for
  *   sorting the returned list.
  * @sort_data: user data for @sort_compare_func.
  *
@@ -3629,7 +3708,7 @@ have_connection_for_device(NMSettings *self, NMDevice *device)
     c_list_for_each_entry (sett_conn, &priv->connections_lst_head, _connections_lst) {
         NMConnection *connection = nm_settings_connection_get_connection(sett_conn);
 
-        if (!nm_device_check_connection_compatible(device, connection, NULL))
+        if (!nm_device_check_connection_compatible(device, connection, TRUE, NULL))
             continue;
 
         if (nm_settings_connection_default_wired_get_device(sett_conn))
diff --git a/src/core/settings/nm-settings.h b/src/core/settings/nm-settings.h
index aba3c565..020623d0 100644
--- a/src/core/settings/nm-settings.h
+++ b/src/core/settings/nm-settings.h
@@ -58,6 +58,8 @@ NMSettings *nm_settings_get(void);
 
 NMSettings *nm_settings_new(NMManager *manager);
 
+NMManager *nm_settings_get_manager(NMSettings *self);
+
 gboolean nm_settings_start(NMSettings *self, GError **error);
 
 typedef void (*NMSettingsAddCallback)(NMSettings            *settings,
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.c
index eb0d733d..0a385247 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-plugin.c
@@ -52,7 +52,6 @@ typedef struct {
 
     GHashTable *unmanaged_specs;
     GHashTable *unrecognized_specs;
-
 } NMSIfcfgRHPluginPrivate;
 
 struct _NMSIfcfgRHPlugin {
@@ -177,6 +176,7 @@ nm_assert_self(NMSIfcfgRHPlugin *self, gboolean unhandled_specs_consistent)
 static NMSIfcfgRHStorage *
 _load_file(NMSIfcfgRHPlugin *self, const char *filename, GError **error)
 {
+    NMSIfcfgRHStorage            *ret            = NULL;
     gs_unref_object NMConnection *connection     = NULL;
     gs_free_error GError         *load_error     = NULL;
     gs_free char                 *unhandled_spec = NULL;
@@ -224,16 +224,16 @@ _load_file(NMSIfcfgRHPlugin *self, const char *filename, GError **error)
             nm_assert_not_reached();
             return NULL;
         }
-        return nms_ifcfg_rh_storage_new_unhandled(self,
+
+        ret = nms_ifcfg_rh_storage_new_unhandled(self, filename, unmanaged_spec, unrecognized_spec);
+    } else {
+        ret = nms_ifcfg_rh_storage_new_connection(self,
                                                   filename,
-                                                  unmanaged_spec,
-                                                  unrecognized_spec);
+                                                  g_steal_pointer(&connection),
+                                                  &st.st_mtim);
     }
 
-    return nms_ifcfg_rh_storage_new_connection(self,
-                                               filename,
-                                               g_steal_pointer(&connection),
-                                               &st.st_mtim);
+    return ret;
 }
 
 static void
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
index 6cfb5705..84a9479d 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c
@@ -917,7 +917,7 @@ enum {
  * @options_route: (in-out): when line is from the OPTIONS setting, this is a pre-created
  *   route object that is completed with the settings from options. Otherwise,
  *   it shall point to %NULL and a new route is created and returned.
- * @out_route: (out) (transfer-full) (allow-none): the parsed %NMIPRoute instance.
+ * @out_route: (out) (transfer full) (optional): the parsed %NMIPRoute instance.
  *   In case a @options_route is passed in, it returns the input route that was modified
  *   in-place. But the caller must unref the returned route in either case.
  * @error: the failure description.
@@ -2481,6 +2481,11 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
         g_object_set(s_ip6, NM_SETTING_IP_CONFIG_DHCP_IAID, v, NULL);
 
     nm_clear_g_free(&value);
+    v = svGetValueStr(ifcfg, "DHCPV6_PD_HINT", &value);
+    if (v)
+        g_object_set(s_ip6, NM_SETTING_IP6_CONFIG_DHCP_PD_HINT, v, NULL);
+
+    nm_clear_g_free(&value);
     v = svGetValueStr(ifcfg, "DHCPV6_HOSTNAME", &value);
     /* Use DHCP_HOSTNAME as fallback if it is in FQDN format and ipv6.method is
      * auto or dhcp: this is required to support old ifcfg files
@@ -2591,7 +2596,7 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
                         &local)) {
         PARSE_WARNING("%s", local->message);
         g_clear_error(&local);
-    } else if (errno == ENOENT) {
+    } else if (errno == ENOKEY) {
         /* The key is not specified. If "v" (IPV6_TOKEN) is set,
          * we default to EUI64. Otherwise, the connection would not verify. */
         if (v)
@@ -2683,16 +2688,25 @@ make_hostname_setting(shvarFile *ifcfg)
     NMTernary  from_dns_lookup;
     NMTernary  only_from_default;
     int        priority;
+    gboolean   has_setting = FALSE;
 
     priority = svGetValueInt64(ifcfg, "HOSTNAME_PRIORITY", 10, G_MININT32, G_MAXINT32, 0);
+    if (!has_setting && errno != ENOKEY)
+        has_setting = TRUE;
+
+    from_dhcp = svGetValueTernary(ifcfg, "HOSTNAME_FROM_DHCP");
+    if (!has_setting && errno != ENOKEY)
+        has_setting = TRUE;
+
+    from_dns_lookup = svGetValueTernary(ifcfg, "HOSTNAME_FROM_DNS_LOOKUP");
+    if (!has_setting && errno != ENOKEY)
+        has_setting = TRUE;
 
-    from_dhcp         = svGetValueTernary(ifcfg, "HOSTNAME_FROM_DHCP");
-    from_dns_lookup   = svGetValueTernary(ifcfg, "HOSTNAME_FROM_DNS_LOOKUP");
     only_from_default = svGetValueTernary(ifcfg, "HOSTNAME_ONLY_FROM_DEFAULT");
+    if (!has_setting && errno != ENOKEY)
+        has_setting = TRUE;
 
-    /* Create the setting when at least one key is not default*/
-    if (priority == 0 && from_dhcp == NM_TERNARY_DEFAULT && from_dns_lookup == NM_TERNARY_DEFAULT
-        && only_from_default == NM_TERNARY_DEFAULT)
+    if (!has_setting)
         return NULL;
 
     setting = nm_setting_hostname_new();
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
index 207b8700..50e352d3 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.c
@@ -881,6 +881,7 @@ const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[] = {
     _KEY_TYPE("DHCPV6_HOSTNAME", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("DHCPV6_HOSTNAME_FLAGS", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("DHCPV6_IAID", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
+    _KEY_TYPE("DHCPV6_PD_HINT", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("DHCPV6_SEND_HOSTNAME", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("DHCP_CLIENT_ID", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
     _KEY_TYPE("DHCP_FQDN", NMS_IFCFG_KEY_TYPE_IS_PLAIN),
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
index 51b118e3..eb9e418a 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-utils.h
@@ -33,7 +33,7 @@ typedef struct {
     NMSIfcfgKeyTypeFlags key_flags;
 } NMSIfcfgKeyTypeInfo;
 
-extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[263];
+extern const NMSIfcfgKeyTypeInfo nms_ifcfg_well_known_keys[264];
 
 const NMSIfcfgKeyTypeInfo *nms_ifcfg_well_known_key_find_info(const char *key, gssize *out_idx);
 
diff --git a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
index 97637063..08deaf5a 100644
--- a/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
+++ b/src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c
@@ -62,6 +62,24 @@
 /*****************************************************************************/
 
 static void
+set_error_unsupported(GError      **error,
+                      NMConnection *connection,
+                      const char   *name,
+                      gboolean      is_setting)
+{
+    g_set_error(error,
+                NM_SETTINGS_ERROR,
+                NM_SETTINGS_ERROR_NOT_SUPPORTED_BY_PLUGIN,
+                "The ifcfg-rh plugin doesn't support %s '%s'. If you are modifying an existing "
+                "connection profile saved in ifcfg-rh format, please migrate the connection to "
+                "keyfile using 'nmcli connection migrate %s' or via the Update2() D-Bus API "
+                "and try again.",
+                is_setting ? "setting" : "property",
+                name,
+                nm_connection_get_uuid(connection));
+};
+
+static void
 save_secret_flags(shvarFile *ifcfg, const char *key, NMSettingSecretFlags flags)
 {
     GString *str;
@@ -3100,6 +3118,9 @@ write_ip6_setting(NMConnection *connection, shvarFile *ifcfg, GString **out_rout
                   "DHCPV6_DUID",
                   nm_setting_ip6_config_get_dhcp_duid(NM_SETTING_IP6_CONFIG(s_ip6)));
     svSetValueStr(ifcfg, "DHCPV6_IAID", nm_setting_ip_config_get_dhcp_iaid(s_ip6));
+    svSetValueStr(ifcfg,
+                  "DHCPV6_PD_HINT",
+                  nm_setting_ip6_config_get_dhcp_pd_hint(NM_SETTING_IP6_CONFIG(s_ip6)));
 
     hostname = nm_setting_ip_config_get_dhcp_hostname(s_ip6);
     svSetValueStr(ifcfg, "DHCPV6_HOSTNAME", hostname);
@@ -3485,6 +3506,11 @@ do_write_construct(NMConnection                   *connection,
     write_sriov_setting(connection, ifcfg);
     write_tc_setting(connection, ifcfg);
 
+    if (_nm_connection_get_setting(connection, NM_TYPE_SETTING_LINK)) {
+        set_error_unsupported(error, connection, "link", TRUE);
+        return FALSE;
+    }
+
     route_path_is_svformat = utils_has_route_file_new_syntax(route_path);
 
     has_complex_routes_v4 = utils_has_complex_routes(ifcfg_name, AF_INET);
diff --git a/src/core/settings/plugins/ifcfg-rh/shvar.c b/src/core/settings/plugins/ifcfg-rh/shvar.c
index fe8187c3..1ca2ea60 100644
--- a/src/core/settings/plugins/ifcfg-rh/shvar.c
+++ b/src/core/settings/plugins/ifcfg-rh/shvar.c
@@ -97,23 +97,32 @@ static void _line_link_parse(shvarFile *s, const char *value, gsize len);
  * in case no valid value is found, the fallback value. Valid values
  * are: "yes", "true", "t", "y", "1" and "no", "false", "f", "n", "0".
  *
+ * Always sets errno. Either to zero on success, to ENOKEY for NULL
+ * or to EINVAL otherwise.
+ *
  * Returns: the parsed boolean value or @fallback.
  */
 int
 svParseBoolean(const char *value, int fallback)
 {
-    if (!value)
+    if (!value) {
+        errno = ENOKEY;
         return fallback;
+    }
 
     if (!g_ascii_strcasecmp("yes", value) || !g_ascii_strcasecmp("true", value)
         || !g_ascii_strcasecmp("t", value) || !g_ascii_strcasecmp("y", value)
-        || !g_ascii_strcasecmp("1", value))
+        || !g_ascii_strcasecmp("1", value)) {
+        errno = 0;
         return TRUE;
-    else if (!g_ascii_strcasecmp("no", value) || !g_ascii_strcasecmp("false", value)
-             || !g_ascii_strcasecmp("f", value) || !g_ascii_strcasecmp("n", value)
-             || !g_ascii_strcasecmp("0", value))
+    } else if (!g_ascii_strcasecmp("no", value) || !g_ascii_strcasecmp("false", value)
+               || !g_ascii_strcasecmp("f", value) || !g_ascii_strcasecmp("n", value)
+               || !g_ascii_strcasecmp("0", value)) {
+        errno = 0;
         return FALSE;
+    }
 
+    errno = EINVAL;
     return fallback;
 }
 
@@ -1253,6 +1262,7 @@ svGetValueStr_cp(shvarFile *s, const char *key)
  * @fallback: the fallback value in any error case
  *
  * Reads a value @key and converts it to a boolean using svParseBoolean().
+ * This always sets errno, see svParseBoolean().
  *
  * Returns: the parsed boolean value or @fallback.
  */
@@ -1271,6 +1281,7 @@ svGetValueBoolean(shvarFile *s, const char *key, int fallback)
  * @key: the name of the key to read
  *
  * Reads a value @key and converts it to a NMTernary value.
+ * This always sets errno, see svParseBoolean().
  *
  * Returns: the parsed NMTernary
  */
@@ -1328,7 +1339,7 @@ svGetValueEnum(shvarFile *s, const char *key, GType gtype, int *out_value, GErro
     if (!svalue) {
         /* don't touch out_value. The caller is supposed
          * to initialize it with the default value. */
-        errno = ENOENT;
+        errno = ENOKEY;
         return TRUE;
     }
 
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
index 1d7de8d2..1679cab6 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-plugin.c
@@ -891,6 +891,7 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
     gboolean                      reread_same;
     const char                   *uuid;
     char                          strbuf[100];
+    NMTernary                     force_rename2;
 
     _nm_assert_storage(self, storage, TRUE);
     nm_assert(NM_IS_CONNECTION(connection));
@@ -910,6 +911,20 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
     previous_filename = nms_keyfile_storage_get_filename(storage);
     uuid              = nms_keyfile_storage_get_uuid(storage);
 
+    if (force_rename)
+        force_rename2 = NM_TERNARY_TRUE;
+    else {
+        /* If the caller does not force a rename, we honor [keyfile].rename
+         * setting, and (if enabled) we rename by following the preferred name
+         * as necessary.  That's indicated with NM_TERNARY_DEFAULT. */
+        force_rename2 = nm_config_data_get_value_boolean(NM_CONFIG_GET_DATA,
+                                                         NM_CONFIG_KEYFILE_GROUP_KEYFILE,
+                                                         NM_CONFIG_KEYFILE_KEY_KEYFILE_RENAME,
+                                                         FALSE)
+                            ? NM_TERNARY_DEFAULT
+                            : NM_TERNARY_FALSE;
+    }
+
     if (!nms_keyfile_writer_connection(
             connection,
             is_nm_generated,
@@ -922,7 +937,7 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
             _get_plugin_dir(priv),
             previous_filename,
             FALSE,
-            FALSE,
+            force_rename2,
             nm_sett_util_allow_filename_cb,
             NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, previous_filename),
             &full_filename,
@@ -938,7 +953,8 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
         return FALSE;
     }
 
-    nm_assert(full_filename && nm_streq(full_filename, previous_filename));
+    nm_assert(full_filename);
+    nm_assert(force_rename2 != NM_TERNARY_FALSE || nm_streq(full_filename, previous_filename));
 
     if (!reread || reread_same)
         nm_g_object_ref_set(&reread, connection);
@@ -957,11 +973,33 @@ nms_keyfile_plugin_update_connection(NMSKeyfilePlugin   *self,
                               "\")",
                               ""));
 
-    storage->u.conn_data.is_nm_generated = is_nm_generated;
-    storage->u.conn_data.is_volatile     = is_volatile;
-    storage->u.conn_data.is_external     = is_external;
-    storage->u.conn_data.stat_mtime      = *nm_sett_util_stat_mtime(full_filename, FALSE, &mtime);
-    storage->u.conn_data.shadowed_owned  = shadowed_owned;
+    nm_sett_util_stat_mtime(full_filename, FALSE, &mtime);
+
+    if (nm_streq(full_filename, previous_filename)) {
+        storage->u.conn_data.is_nm_generated = is_nm_generated;
+        storage->u.conn_data.is_volatile     = is_volatile;
+        storage->u.conn_data.is_external     = is_external;
+        storage->u.conn_data.stat_mtime      = mtime;
+        storage->u.conn_data.shadowed_owned  = shadowed_owned;
+    } else {
+        NMSKeyfileStorage *storage_new;
+
+        /* The filename changed. We cannot modify the filename of an NMSettingsStorage.
+         * We need to create a new one. */
+        storage_new =
+            nms_keyfile_storage_new_connection(NMS_KEYFILE_PLUGIN(storage->parent._plugin),
+                                               g_object_ref(reread),
+                                               full_filename,
+                                               storage->storage_type,
+                                               is_nm_generated,
+                                               is_volatile,
+                                               is_external,
+                                               storage->u.conn_data.shadowed_storage,
+                                               shadowed_owned,
+                                               &mtime);
+        nm_sett_util_storages_add_take(&priv->storages, storage_new);
+        storage = storage_new;
+    }
 
     *out_storage    = g_object_ref(NM_SETTINGS_STORAGE(storage));
     *out_connection = g_steal_pointer(&reread);
@@ -1066,12 +1104,12 @@ delete_connection(NMSettingsPlugin *plugin, NMSettingsStorage *storage_x, GError
  * @shadowed_storage: a tombstone can also shadow an existing storage.
  *   In combination with @set and @in_memory, this is allowed to store
  *   the shadowed storage filename.
- * @out_storage: (transfer full) (allow-none): the storage element that changes, or
- *   NULL if nothing changed. Note that the file on disk is already as
- *   we want to write it, then this still counts as a change. No change only
+ * @out_storage: (transfer full) (optional) (nullable): the storage element that
+ *   changes, or %NULL if nothing changed. Note that the file on disk is already
+ *   as we want to write it, then this still counts as a change. No change only
  *   means if we try to delete a storage (@set %FALSE) that did not
  *   exist previously.
- * @out_hard_failure: (allow-none): on failure, indicate that this is a hard failure.
+ * @out_hard_failure: (optional): on failure, indicate that this is a hard failure.
  *
  * The function writes or deletes nmmeta files to/from filesystem. In this case,
  * the nmmeta files can only be symlinks to /dev/null (to indicate tombstones).
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
index ad6f277c..b1dd2e44 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
@@ -195,7 +195,7 @@ _internal_write_connection(NMConnection                   *connection,
                            pid_t                           owner_grp,
                            const char                     *existing_path,
                            gboolean                        existing_path_read_only,
-                           gboolean                        force_rename,
+                           NMTernary                       force_rename,
                            NMSKeyfileWriterAllowFilenameCb allow_filename_cb,
                            gpointer                        allow_filename_user_data,
                            char                          **out_path,
@@ -212,6 +212,7 @@ _internal_write_connection(NMConnection                   *connection,
     gs_free_error GError           *local_err = NULL;
     int                             errsv;
     gboolean                        rename;
+    gboolean                        rename_follow;
     int                             i_path;
     gs_unref_object NMConnection   *reread      = NULL;
     gboolean                        reread_same = FALSE;
@@ -223,8 +224,12 @@ _internal_write_connection(NMConnection                   *connection,
 
     nm_assert(!shadowed_owned || shadowed_storage);
 
-    rename = force_rename || existing_path_read_only
-             || (existing_path && !nm_utils_file_is_in_path(existing_path, keyfile_dir));
+    rename = existing_path_read_only
+             || (existing_path && !nm_utils_file_is_in_path(existing_path, keyfile_dir))
+             || force_rename == NM_TERNARY_TRUE;
+
+    /* Follow the connection.id upon change. */
+    rename_follow = !rename && existing_path && force_rename == NM_TERNARY_DEFAULT;
 
     id = nm_connection_get_id(connection);
     nm_assert(id && *id);
@@ -283,7 +288,7 @@ _internal_write_connection(NMConnection                   *connection,
         gboolean      is_existing_path;
 
         if (i_path == -2) {
-            if (!existing_path || rename)
+            if (!existing_path || rename || rename_follow)
                 continue;
             path_candidate = g_strdup(existing_path);
         } else if (i_path == -1) {
@@ -427,7 +432,7 @@ nms_keyfile_writer_connection(NMConnection                   *connection,
                               const char                     *profile_dir,
                               const char                     *existing_path,
                               gboolean                        existing_path_read_only,
-                              gboolean                        force_rename,
+                              NMTernary                       force_rename,
                               NMSKeyfileWriterAllowFilenameCb allow_filename_cb,
                               gpointer                        allow_filename_user_data,
                               char                          **out_path,
@@ -458,14 +463,14 @@ nms_keyfile_writer_connection(NMConnection                   *connection,
 }
 
 gboolean
-nms_keyfile_writer_test_connection(NMConnection  *connection,
-                                   const char    *keyfile_dir,
-                                   uid_t          owner_uid,
-                                   pid_t          owner_grp,
-                                   char         **out_path,
-                                   NMConnection **out_reread,
-                                   gboolean      *out_reread_same,
-                                   GError       **error)
+nmtst_keyfile_writer_test_connection(NMConnection  *connection,
+                                     const char    *keyfile_dir,
+                                     uid_t          owner_uid,
+                                     pid_t          owner_grp,
+                                     char         **out_path,
+                                     NMConnection **out_reread,
+                                     gboolean      *out_reread_same,
+                                     GError       **error)
 {
     return _internal_write_connection(connection,
                                       FALSE,
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-writer.h b/src/core/settings/plugins/keyfile/nms-keyfile-writer.h
index 62aaa19d..850d5522 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-writer.h
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-writer.h
@@ -22,7 +22,7 @@ gboolean nms_keyfile_writer_connection(NMConnection                   *connectio
                                        const char                     *profile_dir,
                                        const char                     *existing_path,
                                        gboolean                        existing_path_read_only,
-                                       gboolean                        force_rename,
+                                       NMTernary                       force_rename,
                                        NMSKeyfileWriterAllowFilenameCb allow_filename_cb,
                                        gpointer                        allow_filename_user_data,
                                        char                          **out_path,
@@ -30,13 +30,13 @@ gboolean nms_keyfile_writer_connection(NMConnection                   *connectio
                                        gboolean                       *out_reread_same,
                                        GError                        **error);
 
-gboolean nms_keyfile_writer_test_connection(NMConnection  *connection,
-                                            const char    *keyfile_dir,
-                                            uid_t          owner_uid,
-                                            pid_t          owner_grp,
-                                            char         **out_path,
-                                            NMConnection **out_reread,
-                                            gboolean      *out_reread_same,
-                                            GError       **error);
+gboolean nmtst_keyfile_writer_test_connection(NMConnection  *connection,
+                                              const char    *keyfile_dir,
+                                              uid_t          owner_uid,
+                                              pid_t          owner_grp,
+                                              char         **out_path,
+                                              NMConnection **out_reread,
+                                              gboolean      *out_reread_same,
+                                              GError       **error);
 
 #endif /* __NMS_KEYFILE_WRITER_H__ */
diff --git a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
index 83019bab..866b1ffd 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -138,14 +138,14 @@ write_test_connection_reread(NMConnection  *connection,
 
     connection_normalized = nmtst_connection_duplicate_and_normalize(connection);
 
-    success = nms_keyfile_writer_test_connection(connection_normalized,
-                                                 TEST_SCRATCH_DIR,
-                                                 owner_uid,
-                                                 owner_grp,
-                                                 testfile,
-                                                 out_reread,
-                                                 out_reread_same,
-                                                 p_error);
+    success = nmtst_keyfile_writer_test_connection(connection_normalized,
+                                                   TEST_SCRATCH_DIR,
+                                                   owner_uid,
+                                                   owner_grp,
+                                                   testfile,
+                                                   out_reread,
+                                                   out_reread_same,
+                                                   p_error);
     g_assert_no_error(error);
     g_assert(success);
     g_assert(*testfile && (*testfile)[0]);
diff --git a/src/core/supplicant/nm-supplicant-manager.c b/src/core/supplicant/nm-supplicant-manager.c
index f6927500..3b805693 100644
--- a/src/core/supplicant/nm-supplicant-manager.c
+++ b/src/core/supplicant/nm-supplicant-manager.c
@@ -447,7 +447,7 @@ _create_iface_dbus_call_get_interface_cb(GObject *source, GAsyncResult *result,
     nm_assert(handle->name_owner == priv->name_owner);
 
     if (!res) {
-        char ifname[NMP_IFNAMSIZ];
+        char ifname[NM_IFNAMSIZ];
 
         if (handle->create_iface_try_count < CREATE_IFACE_TRY_COUNT_MAX
             && nm_dbus_error_is(error, NM_WPAS_ERROR_UNKNOWN_IFACE)
@@ -489,7 +489,7 @@ _create_iface_dbus_call_create_interface_cb(GObject      *source,
     gs_unref_variant GVariant   *res   = NULL;
     gs_free_error GError        *error = NULL;
     const char                  *iface_path_str;
-    char                         ifname[NMP_IFNAMSIZ];
+    char                         ifname[NM_IFNAMSIZ];
 
     res = g_dbus_connection_call_finish(dbus_connection, result, &error);
 
@@ -619,7 +619,7 @@ static void
 _create_iface_dbus_start(NMSupplicantManager *self, NMSupplMgrCreateIfaceHandle *handle)
 {
     NMSupplicantManagerPrivate *priv = NM_SUPPLICANT_MANAGER_GET_PRIVATE(self);
-    char                        ifname[NMP_IFNAMSIZ];
+    char                        ifname[NM_IFNAMSIZ];
 
     nm_assert(priv->name_owner);
     nm_assert(!handle->cancellable);
diff --git a/src/core/tests/test-core-with-expect.c b/src/core/tests/test-core-with-expect.c
index b05144ac..01510126 100644
--- a/src/core/tests/test-core-with-expect.c
+++ b/src/core/tests/test-core-with-expect.c
@@ -333,9 +333,8 @@ do_test_nm_utils_kill_child(void)
 
     /* pid3s should not be a valid process, hence the call should fail. Note, that there
      * is a race here. */
-    NMTST_EXPECT_NM_ERROR(
-        "kill child process 'test-s-3-2' (*): failed due to unexpected return value -1 by waitpid "
-        "(No child process*, 10) after sending no signal (0)");
+    NMTST_EXPECT_NM_ERROR("kill child process 'test-s-3-2' (*): unexpected error while waitpid: No "
+                          "child process* (10)");
     test_nm_utils_kill_child_sync_do("test-s-3-2", pid3s, 0, 0, FALSE, NULL);
 
     NMTST_EXPECT_NM_DEBUG("kill child process 'test-s-4' (*): waiting up to 50 milliseconds for "
@@ -396,9 +395,8 @@ do_test_nm_utils_kill_child(void)
 
     /* pid3a should not be a valid process, hence the call should fail. Note, that there
      * is a race here. */
-    NMTST_EXPECT_NM_ERROR(
-        "kill child process 'test-a-3-2' (*): failed due to unexpected return value -1 by waitpid "
-        "(No child process*, 10) after sending no signal (0)");
+    NMTST_EXPECT_NM_ERROR("kill child process 'test-a-3-2' (*): unexpected error while "
+                          "waitpid: No child process* (10)");
     NMTST_EXPECT_NM_DEBUG(
         "kill child process 'test-a-3-2' (*): invoke callback: killing child failed");
     test_nm_utils_kill_child_async_do("test-a-3-2", pid3a, 0, 0, FALSE, NULL);
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index 887803bf..8296d745 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -18,6 +18,7 @@
 
 #include "dns/nm-dns-manager.h"
 #include "nm-connectivity.h"
+#include "nm-firewall-utils.h"
 
 #include "nm-test-utils-core.h"
 
@@ -1243,13 +1244,9 @@ _test_match_spec_device(const GSList *specs, const char *match_str)
 {
     if (match_str && g_str_has_prefix(match_str, MATCH_S390))
         return nm_match_spec_device(specs,
-                                    NULL,
-                                    NULL,
-                                    NULL,
-                                    NULL,
-                                    NULL,
-                                    &match_str[NM_STRLEN(MATCH_S390)],
-                                    NULL);
+                                    &((const NMMatchSpecDeviceData){
+                                        .s390_subchannels = &match_str[NM_STRLEN(MATCH_S390)],
+                                    }));
     if (match_str && g_str_has_prefix(match_str, MATCH_DRIVER)) {
         gs_free char *s = g_strdup(&match_str[NM_STRLEN(MATCH_DRIVER)]);
         char         *t;
@@ -1259,9 +1256,16 @@ _test_match_spec_device(const GSList *specs, const char *match_str)
             t[0] = '\0';
             t++;
         }
-        return nm_match_spec_device(specs, NULL, NULL, s, t, NULL, NULL, NULL);
+        return nm_match_spec_device(specs,
+                                    &((const NMMatchSpecDeviceData){
+                                        .driver         = s,
+                                        .driver_version = t,
+                                    }));
     }
-    return nm_match_spec_device(specs, match_str, NULL, NULL, NULL, NULL, NULL, NULL);
+    return nm_match_spec_device(specs,
+                                &((const NMMatchSpecDeviceData){
+                                    .interface_name = match_str,
+                                }));
 }
 
 static void
@@ -2109,7 +2113,7 @@ do_test_stable_id_parse(const char       *stable_id,
         g_assert(!expected_generated);
 
     if (expected_stable_type == NM_UTILS_STABLE_TYPE_UUID)
-        g_assert(!stable_id);
+        g_assert(NM_IN_STRSET(stable_id, NULL, "default${CONNECTION}"));
     else
         g_assert(stable_id);
 
@@ -2137,6 +2141,7 @@ test_stable_id_parse(void)
 #define _parse_random(stable_id) \
     do_test_stable_id_parse("" stable_id "", NM_UTILS_STABLE_TYPE_RANDOM, NULL)
     do_test_stable_id_parse(NULL, NM_UTILS_STABLE_TYPE_UUID, NULL);
+    do_test_stable_id_parse("default${CONNECTION}", NM_UTILS_STABLE_TYPE_UUID, NULL);
     _parse_stable_id("");
     _parse_stable_id("a");
     _parse_stable_id("a$");
@@ -2151,6 +2156,7 @@ test_stable_id_parse(void)
     _parse_stable_id("a$${CONNECTION}");
     _parse_stable_id("a$${CONNECTION}x");
     _parse_generated("${CONNECTION}", "${CONNECTION}=11{_CONNECTION}");
+    _parse_generated(" ${CONNECTION}", " ${CONNECTION}=11{_CONNECTION}");
     _parse_generated("${${CONNECTION}", "${${CONNECTION}=11{_CONNECTION}");
     _parse_generated("${CONNECTION}x", "${CONNECTION}=11{_CONNECTION}x");
     _parse_generated("x${CONNECTION}", "x${CONNECTION}=11{_CONNECTION}");
@@ -2580,6 +2586,125 @@ test_connectivity_state_cmp(void)
 
 /*****************************************************************************/
 
+static void
+test_nm_firewall_nft_stdio_mlag(void)
+{
+#define _T(up,                                               \
+           bond_ifname,                                      \
+           bond_ifnames_down,                                \
+           active_members,                                   \
+           previous_members,                                 \
+           with_counters,                                    \
+           expected)                                         \
+    G_STMT_START                                             \
+    {                                                        \
+        gs_unref_bytes GBytes *_b = NULL;                    \
+                                                             \
+        _b = nm_firewall_nft_stdio_mlag((up),                \
+                                        (bond_ifname),       \
+                                        (bond_ifnames_down), \
+                                        (active_members),    \
+                                        (previous_members),  \
+                                        (with_counters));    \
+                                                             \
+        g_assert(_b);                                        \
+        nmtst_assert_cmpmem(expected,                        \
+                            NM_STRLEN(expected),             \
+                            g_bytes_get_data(_b, NULL),      \
+                            g_bytes_get_size(_b));           \
+    }                                                        \
+    G_STMT_END
+
+    _T(TRUE,
+       "bond0",
+       NM_MAKE_STRV("eth0"),
+       NM_MAKE_STRV("eth1"),
+       NM_MAKE_STRV("eth2"),
+       TRUE,
+       "add table netdev nm-mlag-eth0\012delete table netdev nm-mlag-eth0\012add table netdev "
+       "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
+       "macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; }\012add set netdev "
+       "nm-mlag-bond0 macset-untagged { typeof ether saddr; flags dynamic,timeout; }\012add chain "
+       "netdev nm-mlag-bond0 tx-snoop-source-mac { type filter hook egress device bond0 priority "
+       "filter; }\012add rule netdev nm-mlag-bond0 tx-snoop-source-mac set update ether saddr . "
+       "vlan id timeout 5s @macset-tagged counter return\012add rule netdev nm-mlag-bond0 "
+       "tx-snoop-source-mac set update ether saddr timeout 5s @macset-untagged counter\012add "
+       "chain netdev nm-mlag-bond0 rx-drop-looped-packets { type filter hook ingress device bond0 "
+       "priority filter; }\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr . "
+       "vlan id @macset-tagged counter drop\012add rule netdev nm-mlag-bond0 "
+       "rx-drop-looped-packets ether type vlan counter return\012add rule netdev nm-mlag-bond0 "
+       "rx-drop-looped-packets ether saddr @macset-untagged counter drop\012");
+
+    _T(TRUE,
+       "bond0",
+       NM_MAKE_STRV("eth0"),
+       NM_MAKE_STRV("eth1"),
+       NM_MAKE_STRV("eth2"),
+       FALSE,
+       "add table netdev nm-mlag-eth0\012delete table netdev nm-mlag-eth0\012add table netdev "
+       "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth1 { type filter hook ingress device eth1 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth1\012add set netdev nm-mlag-bond0 "
+       "macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; }\012add set netdev "
+       "nm-mlag-bond0 macset-untagged { typeof ether saddr; flags dynamic,timeout; }\012add chain "
+       "netdev nm-mlag-bond0 tx-snoop-source-mac { type filter hook egress device bond0 priority "
+       "filter; }\012add rule netdev nm-mlag-bond0 tx-snoop-source-mac set update ether saddr . "
+       "vlan id timeout 5s @macset-tagged return\012add rule netdev nm-mlag-bond0 "
+       "tx-snoop-source-mac set update ether saddr timeout 5s @macset-untagged\012add chain netdev "
+       "nm-mlag-bond0 rx-drop-looped-packets { type filter hook ingress device bond0 priority "
+       "filter; }\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr . vlan id "
+       "@macset-tagged drop\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether type "
+       "vlan return\012add rule netdev nm-mlag-bond0 rx-drop-looped-packets ether saddr "
+       "@macset-untagged drop\012");
+
+    _T(TRUE,
+       "bond0",
+       NM_MAKE_STRV("eth0", "eth1"),
+       NM_MAKE_STRV("eth2", "eth3"),
+       NM_MAKE_STRV("eth4", "eth5"),
+       FALSE,
+       "add table netdev nm-mlag-eth0\012delete table netdev nm-mlag-eth0\012add table netdev "
+       "nm-mlag-eth1\012delete table netdev nm-mlag-eth1\012add table netdev "
+       "nm-mlag-bond0\012flush table netdev nm-mlag-bond0\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth4 { type filter hook ingress device eth4 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth4\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth5 { type filter hook ingress device eth5 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth5\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth2 { type filter hook ingress device eth2 priority filter; }\012delete "
+       "chain netdev nm-mlag-bond0 rx-drop-bc-mc-eth2\012add chain netdev nm-mlag-bond0 "
+       "rx-drop-bc-mc-eth3 { type filter hook ingress device eth3 priority filter; }\012add rule "
+       "netdev nm-mlag-bond0 rx-drop-bc-mc-eth3 pkttype { broadcast, multicast } drop\012add set "
+       "netdev nm-mlag-bond0 macset-tagged { typeof ether saddr . vlan id; flags dynamic,timeout; "
+       "}\012add set netdev nm-mlag-bond0 macset-untagged { typeof ether saddr; flags "
+       "dynamic,timeout; }\012add chain netdev nm-mlag-bond0 tx-snoop-source-mac { type filter "
+       "hook egress device bond0 priority filter; }\012add rule netdev nm-mlag-bond0 "
+       "tx-snoop-source-mac set update ether saddr . vlan id timeout 5s @macset-tagged "
+       "return\012add rule netdev nm-mlag-bond0 tx-snoop-source-mac set update ether saddr timeout "
+       "5s @macset-untagged\012add chain netdev nm-mlag-bond0 rx-drop-looped-packets { type filter "
+       "hook ingress device bond0 priority filter; }\012add rule netdev nm-mlag-bond0 "
+       "rx-drop-looped-packets ether saddr . vlan id @macset-tagged drop\012add rule netdev "
+       "nm-mlag-bond0 rx-drop-looped-packets ether type vlan return\012add rule netdev "
+       "nm-mlag-bond0 rx-drop-looped-packets ether saddr @macset-untagged drop\012");
+
+    _T(FALSE,
+       "bond0",
+       NM_MAKE_STRV("eth0", "eth1"),
+       NM_MAKE_STRV("eth2", "eth3"),
+       NM_MAKE_STRV("eth4", "eth5"),
+       FALSE,
+       "add table netdev nm-mlag-eth0\012delete table netdev nm-mlag-eth0\012add table netdev "
+       "nm-mlag-eth1\012delete table netdev nm-mlag-eth1\012add table netdev "
+       "nm-mlag-bond0\012delete table netdev nm-mlag-bond0\012");
+}
+
+/*****************************************************************************/
+
 NMTST_DEFINE();
 
 int
@@ -2654,5 +2779,7 @@ main(int argc, char **argv)
     g_test_add_func("/core/general/test_kernel_cmdline_match_check",
                     test_kernel_cmdline_match_check);
 
+    g_test_add_func("/core/test_nm_firewall_nft_stdio_mlag", test_nm_firewall_nft_stdio_mlag);
+
     return g_test_run();
 }