about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2024-01-25 09:46:18 +0100
committerMichael Biebl <biebl@debian.org>2024-01-25 09:46:18 +0100
commit70e18d99b8e3e77bb37e218d7ac582130156f8ef (patch)
treed40c587e6d3f0e094ff558e415f1bb9803643214 /src/core
parentd4d8b2b91f7ba000d97a8b2aab48c85000c11314 (diff)
New upstream version 1.45.90 upstream/1.45.90
Diffstat (limited to 'src/core')
-rw-r--r--src/core/NetworkManagerUtils.c35
-rw-r--r--src/core/devices/adsl/nm-device-adsl.c6
-rw-r--r--src/core/devices/bluetooth/nm-device-bt.c6
-rw-r--r--src/core/devices/nm-device-6lowpan.c6
-rw-r--r--src/core/devices/nm-device-bond.c25
-rw-r--r--src/core/devices/nm-device-bridge.c195
-rw-r--r--src/core/devices/nm-device-dummy.c8
-rw-r--r--src/core/devices/nm-device-ethernet.c42
-rw-r--r--src/core/devices/nm-device-factory.c1
-rw-r--r--src/core/devices/nm-device-generic.c6
-rw-r--r--src/core/devices/nm-device-hsr.c287
-rw-r--r--src/core/devices/nm-device-hsr.h31
-rw-r--r--src/core/devices/nm-device-infiniband.c12
-rw-r--r--src/core/devices/nm-device-tun.c8
-rw-r--r--src/core/devices/nm-device-vlan.c12
-rw-r--r--src/core/devices/nm-device-vxlan.c6
-rw-r--r--src/core/devices/nm-device-wireguard.c4
-rw-r--r--src/core/devices/nm-device-wpan.c8
-rw-r--r--src/core/devices/nm-device.c535
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c6
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.c340
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c6
-rw-r--r--src/core/devices/team/nm-device-team.c22
-rw-r--r--src/core/devices/wifi/nm-device-iwd-p2p.c6
-rw-r--r--src/core/devices/wifi/nm-device-olpc-mesh.c6
-rw-r--r--src/core/devices/wifi/nm-device-wifi-p2p.c89
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c35
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c9
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.c41
-rw-r--r--src/core/devices/wifi/nm-wifi-ap.h3
-rw-r--r--src/core/devices/wifi/nm-wifi-common.c11
-rw-r--r--src/core/dhcp/nm-dhcp-client.c2
-rw-r--r--src/core/dhcp/nm-dhcp-client.h3
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.c12
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.h1
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c125
-rw-r--r--src/core/dhcp/nm-dhcp-dhcpcd.c2
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c4
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c40
-rw-r--r--src/core/dhcp/tests/test-dhcp-dhclient.c100
-rw-r--r--src/core/dns/nm-dns-manager.c42
-rw-r--r--src/core/dns/nm-dns-manager.h2
-rw-r--r--src/core/dnsmasq/nm-dnsmasq-utils.c4
-rw-r--r--src/core/main-utils.c5
-rw-r--r--src/core/meson.build1
-rw-r--r--src/core/ndisc/nm-fake-ndisc.c2
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.c2
-rw-r--r--src/core/ndisc/nm-ndisc.c4
-rw-r--r--src/core/nm-act-request.c13
-rw-r--r--src/core/nm-active-connection.c11
-rw-r--r--src/core/nm-active-connection.h3
-rw-r--r--src/core/nm-auth-utils.c11
-rw-r--r--src/core/nm-config-data.c4
-rw-r--r--src/core/nm-config.c8
-rw-r--r--src/core/nm-connectivity.c17
-rw-r--r--src/core/nm-core-utils.c196
-rw-r--r--src/core/nm-core-utils.h7
-rw-r--r--src/core/nm-dbus-manager.c4
-rw-r--r--src/core/nm-dbus-utils.c12
-rw-r--r--src/core/nm-dbus-utils.h59
-rw-r--r--src/core/nm-dispatcher.c51
-rw-r--r--src/core/nm-dispatcher.h3
-rw-r--r--src/core/nm-ip-config.c121
-rw-r--r--src/core/nm-ip-config.h5
-rw-r--r--src/core/nm-l3-config-data.c62
-rw-r--r--src/core/nm-l3-config-data.h2
-rw-r--r--src/core/nm-l3-ipv4ll.c68
-rw-r--r--src/core/nm-l3cfg.c442
-rw-r--r--src/core/nm-l3cfg.h4
-rw-r--r--src/core/nm-manager.c69
-rw-r--r--src/core/nm-netns.c35
-rw-r--r--src/core/nm-policy.c12
-rw-r--r--src/core/nm-rfkill-manager.c25
-rw-r--r--src/core/nm-rfkill-manager.h7
-rw-r--r--src/core/nm-types.h2
-rw-r--r--src/core/platform/nm-fake-platform.c8
-rw-r--r--src/core/platform/tests/test-common.h2
-rw-r--r--src/core/platform/tests/test-link.c41
-rw-r--r--src/core/platform/tests/test-platform-general.c2
-rw-r--r--src/core/settings/nm-settings.c2
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c10
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c43
-rw-r--r--src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c19
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c10
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c15
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c2
-rw-r--r--src/core/supplicant/nm-supplicant-types.h8
-rw-r--r--src/core/tests/test-core-with-expect.c2
-rw-r--r--src/core/tests/test-core.c56
-rw-r--r--src/core/tests/test-l3cfg.c2
-rw-r--r--src/core/vpn/nm-vpn-connection.c3
91 files changed, 2626 insertions, 1010 deletions
diff --git a/src/core/NetworkManagerUtils.c b/src/core/NetworkManagerUtils.c
index 8606082c..cced1717 100644
--- a/src/core/NetworkManagerUtils.c
+++ b/src/core/NetworkManagerUtils.c
@@ -120,7 +120,7 @@ get_new_connection_name(NMConnection *const *existing_connections,
          * connection id. */
         temp = g_strdup_printf(C_("connection id fallback", "%s %u"), fallback_prefix, i);
 
-        if (nm_strv_find_first(existing_names, existing_len, temp) < 0)
+        if (!nm_strv_contains(existing_names, existing_len, temp))
             return temp;
 
         g_free(temp);
@@ -670,9 +670,9 @@ check_connection_cloned_mac_address(NMConnection *orig,
         cand_mac = nm_setting_wired_get_cloned_mac_address(s_wired_cand);
 
     /* special cloned mac address entries are accepted. */
-    if (NM_CLONED_MAC_IS_SPECIAL(orig_mac))
+    if (NM_CLONED_MAC_IS_SPECIAL(orig_mac, FALSE))
         orig_mac = NULL;
-    if (NM_CLONED_MAC_IS_SPECIAL(cand_mac))
+    if (NM_CLONED_MAC_IS_SPECIAL(cand_mac, FALSE))
         cand_mac = NULL;
 
     if (!orig_mac || !cand_mac) {
@@ -695,7 +695,8 @@ check_connection_controller(NMConnection *orig, NMConnection *candidate, GHashTa
 
     props = check_property_in_hash(settings,
                                    NM_SETTING_CONNECTION_SETTING_NAME,
-                                   NM_SETTING_CONNECTION_MASTER);
+                                   NM_SETTING_CONNECTION_CONTROLLER);
+
     if (!props)
         return TRUE;
 
@@ -722,6 +723,10 @@ check_connection_controller(NMConnection *orig, NMConnection *candidate, GHashTa
                                  props,
                                  NM_SETTING_CONNECTION_SETTING_NAME,
                                  NM_SETTING_CONNECTION_MASTER);
+                remove_from_hash(settings,
+                                 props,
+                                 NM_SETTING_CONNECTION_SETTING_NAME,
+                                 NM_SETTING_CONNECTION_CONTROLLER);
                 return TRUE;
             } else {
                 return FALSE;
@@ -892,8 +897,8 @@ nm_utils_match_connection(NMConnection *const   *connections,
             if (!nm_streq0(nm_setting_connection_get_connection_type(s_orig),
                            nm_setting_connection_get_connection_type(s_cand)))
                 continue;
-            if (!nm_streq0(nm_setting_connection_get_slave_type(s_orig),
-                           nm_setting_connection_get_slave_type(s_cand)))
+            if (!nm_streq0(nm_setting_connection_get_port_type(s_orig),
+                           nm_setting_connection_get_port_type(s_cand)))
                 continue;
 
             /* this is good enough for a match */
@@ -1558,7 +1563,8 @@ nm_utils_ip_addresses_to_dbus(int                          addr_family,
     char             addr_str[NM_INET_ADDRSTRLEN];
     NMDedupMultiIter iter;
     const NMPObject *obj;
-    guint            i;
+    const gsize      MAX_ADDRESSES = 100;
+    gsize            i;
 
     nm_assert_addr_family(addr_family);
 
@@ -1580,6 +1586,11 @@ nm_utils_ip_addresses_to_dbus(int                          addr_family,
         nm_platform_dedup_multi_iter_next_obj(&iter, &obj, NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4))) {
         const NMPlatformIPXAddress *address = NMP_OBJECT_CAST_IPX_ADDRESS(obj);
 
+        if (i > MAX_ADDRESSES) {
+            /* Limited. The rest is hidden. */
+            break;
+        }
+
         if (out_address_data) {
             GVariantBuilder addr_builder;
             gconstpointer   p;
@@ -1669,6 +1680,8 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
     GVariantBuilder  builder_data;
     GVariantBuilder  builder_legacy;
     char             addr_str[NM_INET_ADDRSTRLEN];
+    const gsize      MAX_ROUTES = 100;
+    gsize            i;
 
     nm_assert_addr_family(addr_family);
 
@@ -1681,6 +1694,7 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
             g_variant_builder_init(&builder_legacy, G_VARIANT_TYPE("a(ayuayu)"));
     }
 
+    i = 0;
     nm_dedup_multi_iter_init(&iter, head_entry);
     while (nm_platform_dedup_multi_iter_next_obj(&iter, &obj, NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4))) {
         const NMPlatformIPXRoute *r = NMP_OBJECT_CAST_IPX_ROUTE(obj);
@@ -1699,6 +1713,13 @@ nm_utils_ip_routes_to_dbus(int                          addr_family,
         if (r->rx.type_coerced != nm_platform_route_type_coerce(RTN_UNICAST))
             continue;
 
+        if (i >= MAX_ROUTES) {
+            /* Limited. The rest is hidden. */
+            break;
+        }
+
+        i++;
+
         if (out_route_data) {
             GVariantBuilder route_builder;
             gconstpointer   gateway;
diff --git a/src/core/devices/adsl/nm-device-adsl.c b/src/core/devices/adsl/nm-device-adsl.c
index 89bc84d8..ba605077 100644
--- a/src/core/devices/adsl/nm-device-adsl.c
+++ b/src/core/devices/adsl/nm-device-adsl.c
@@ -683,7 +683,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_adsl = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_ADSL,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/bluetooth/nm-device-bt.c b/src/core/devices/bluetooth/nm-device-bt.c
index 8b13e97d..ce110aa0 100644
--- a/src/core/devices/bluetooth/nm-device-bt.c
+++ b/src/core/devices/bluetooth/nm-device-bt.c
@@ -1306,7 +1306,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_bluetooth = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_BLUETOOTH,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Name", "s", NM_DEVICE_BT_NAME),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("BtCapabilities",
                                                            "u",
diff --git a/src/core/devices/nm-device-6lowpan.c b/src/core/devices/nm-device-6lowpan.c
index f3386ddb..78ec634c 100644
--- a/src/core/devices/nm-device-6lowpan.c
+++ b/src/core/devices/nm-device-6lowpan.c
@@ -212,7 +212,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_6lowpan = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_6LOWPAN,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Parent", "o", NM_DEVICE_PARENT), ), ),
 };
 
diff --git a/src/core/devices/nm-device-bond.c b/src/core/devices/nm-device-bond.c
index 10fe8092..a3467930 100644
--- a/src/core/devices/nm-device-bond.c
+++ b/src/core/devices/nm-device-bond.c
@@ -248,9 +248,9 @@ controller_update_port_connection(NMDevice     *self,
                      NULL);
 
     g_object_set(nm_connection_get_setting_connection(connection),
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  nm_connection_get_uuid(applied_connection),
-                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_CONNECTION_PORT_TYPE,
                  NM_SETTING_BOND_SETTING_NAME,
                  NULL);
     return TRUE;
@@ -292,7 +292,7 @@ set_arp_targets(NMDevice *device, const char *cur_arp_ip_target, const char *new
                 }
             }
 
-            if (nm_strv_find_first(new_strv, i, s) < 0)
+            if (!nm_strv_contains(new_strv, i, s))
                 new_strv[j++] = s;
         }
         new_strv[j] = NULL;
@@ -683,6 +683,7 @@ commit_port_options(NMDevice *bond_device, NMDevice *port, NMSettingBondPort *s_
                                 .prio     = prio_has ? prio : 0,
                                 .prio_has = prio_has,
                             }),
+                            NULL,
                             0);
 }
 
@@ -928,9 +929,21 @@ static const NMDBusInterfaceInfoExtended interface_info_device_bond = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_BOND,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Slaves",
+                "ao",
+                NM_DEVICE_SLAVES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index 9a45dbf3..193b655c 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -421,22 +421,6 @@ static const Option master_options[] = {
         0,
     }};
 
-static const Option slave_options[] = {
-    OPTION(NM_SETTING_BRIDGE_PORT_PRIORITY,
-           "priority",
-           OPTION_TYPE_INT(NM_BRIDGE_PORT_PRIORITY_MIN,
-                           NM_BRIDGE_PORT_PRIORITY_MAX,
-                           NM_BRIDGE_PORT_PRIORITY_DEF),
-           .default_if_zero = TRUE, ),
-    OPTION(NM_SETTING_BRIDGE_PORT_PATH_COST,
-           "path_cost",
-           OPTION_TYPE_INT(NM_BRIDGE_PORT_PATH_COST_MIN,
-                           NM_BRIDGE_PORT_PATH_COST_MAX,
-                           NM_BRIDGE_PORT_PATH_COST_DEF),
-           .default_if_zero = TRUE, ),
-    OPTION(NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE, "hairpin_mode", OPTION_TYPE_BOOL(FALSE), ),
-    {0}};
-
 static const NMPlatformBridgeVlan **
 setting_vlans_to_platform(GPtrArray *array)
 {
@@ -473,90 +457,26 @@ setting_vlans_to_platform(GPtrArray *array)
 static void
 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++) {
-        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);
-    }
+    guint32 path_cost, priority;
+
+    path_cost = nm_setting_bridge_port_get_path_cost(setting);
+    if (path_cost == 0)
+        path_cost = NM_BRIDGE_PORT_PATH_COST_DEF;
+
+    priority = nm_setting_bridge_port_get_priority(setting);
+    if (priority == 0)
+        priority = NM_BRIDGE_PORT_PRIORITY_DEF;
+
+    nm_platform_link_change(nm_device_get_platform(device),
+                            nm_device_get_ifindex(device),
+                            NULL,
+                            NULL,
+                            &((NMPlatformLinkBridgePort){
+                                .path_cost = path_cost,
+                                .priority  = priority,
+                                .hairpin   = nm_setting_bridge_port_get_hairpin_mode(setting),
+                            }),
+                            0);
 }
 
 static void
@@ -662,50 +582,37 @@ master_update_slave_connection(NMDevice     *device,
                                NMConnection *connection,
                                GError      **error)
 {
-    NMDeviceBridge      *self = NM_DEVICE_BRIDGE(device);
-    NMSettingConnection *s_con;
-    NMSettingBridgePort *s_port;
-    int                  ifindex_slave      = nm_device_get_ifindex(slave);
-    NMConnection        *applied_connection = nm_device_get_applied_connection(device);
-
-    const Option *option;
+    NMSettingConnection  *s_con;
+    NMSettingBridgePort  *s_port;
+    int                   ifindex_slave      = nm_device_get_ifindex(slave);
+    NMConnection         *applied_connection = nm_device_get_applied_connection(device);
+    const NMPlatformLink *pllink;
 
     g_return_val_if_fail(ifindex_slave > 0, FALSE);
 
     s_con  = nm_connection_get_setting_connection(connection);
     s_port = _nm_connection_ensure_setting(connection, NM_TYPE_SETTING_BRIDGE_PORT);
-
-    for (option = slave_options; option->name; option++) {
-        gs_free char *str = nm_platform_sysctl_slave_get_option(nm_device_get_platform(device),
-                                                                ifindex_slave,
-                                                                option->sysname);
-        uint          value;
-
-        if (str) {
-            /* See comments in set_sysfs_uint() about centiseconds. */
-            if (option->user_hz_compensate) {
-                value = _nm_utils_ascii_str_to_int64(str,
-                                                     10,
-                                                     option->nm_min * 100,
-                                                     option->nm_max * 100,
-                                                     option->nm_default * 100);
-                value /= 100;
-            } else {
-                value = _nm_utils_ascii_str_to_int64(str,
-                                                     10,
-                                                     option->nm_min,
-                                                     option->nm_max,
-                                                     option->nm_default);
-            }
-            g_object_set(s_port, option->name, value, NULL);
-        } else
-            _LOGW(LOGD_BRIDGE, "failed to read bridge port setting '%s'", option->sysname);
+    pllink = nm_platform_link_get(nm_device_get_platform(slave), ifindex_slave);
+
+    if (pllink && pllink->port_kind == NM_PORT_KIND_BRIDGE) {
+        g_object_set(s_port,
+                     NM_SETTING_BRIDGE_PORT_PATH_COST,
+                     pllink->port_data.bridge.path_cost,
+                     NULL);
+        g_object_set(s_port,
+                     NM_SETTING_BRIDGE_PORT_PRIORITY,
+                     pllink->port_data.bridge.priority,
+                     NULL);
+        g_object_set(s_port,
+                     NM_SETTING_BRIDGE_PORT_HAIRPIN_MODE,
+                     pllink->port_data.bridge.hairpin,
+                     NULL);
     }
 
     g_object_set(s_con,
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  nm_connection_get_uuid(applied_connection),
-                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_CONNECTION_PORT_TYPE,
                  NM_SETTING_BRIDGE_SETTING_NAME,
                  NULL);
     return TRUE;
@@ -1234,9 +1141,21 @@ static const NMDBusInterfaceInfoExtended interface_info_device_bridge = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_BRIDGE,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Slaves",
+                "ao",
+                NM_DEVICE_SLAVES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-dummy.c b/src/core/devices/nm-device-dummy.c
index 36a0ee6f..b7c4106a 100644
--- a/src/core/devices/nm-device-dummy.c
+++ b/src/core/devices/nm-device-dummy.c
@@ -107,9 +107,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_dummy = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_DUMMY,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
-                                                           "s",
-                                                           NM_DEVICE_HW_ADDRESS), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index aedacc24..0f7f9d65 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -1431,7 +1431,7 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
 
                 mtu = nm_setting_ppp_get_mtu(s_ppp);
                 mru = nm_setting_ppp_get_mru(s_ppp);
-                mxu = MAX(mru, mtu);
+                mxu = NM_MAX(mru, mtu);
                 if (mxu) {
                     _LOGD(LOGD_PPP,
                           "set MTU to %u (PPP interface MRU %u, MTU %u)",
@@ -1711,10 +1711,8 @@ new_default_connection(NMDevice *self)
     NMSettingsConnection *const   *connections;
     NMSetting                     *setting;
     gs_unref_hashtable GHashTable *existing_ids = NULL;
-    struct udev_device            *dev;
     const char                    *perm_hw_addr;
     const char                    *iface;
-    const char                    *uprop   = "0";
     gs_free char                  *defname = NULL;
     gs_free char                  *uuid    = NULL;
     guint                          i, n_connections;
@@ -1760,30 +1758,6 @@ new_default_connection(NMDevice *self)
                  iface,
                  NULL);
 
-    /* Check if we should create a Link-Local only connection */
-    dev = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)),
-                                           nm_device_get_ip_ifindex(self));
-    if (dev)
-        uprop = udev_device_get_property_value(dev, "NM_AUTO_DEFAULT_LINK_LOCAL_ONLY");
-
-    if (_nm_utils_ascii_str_to_bool(uprop, FALSE)) {
-        setting = nm_setting_ip4_config_new();
-        g_object_set(setting,
-                     NM_SETTING_IP_CONFIG_METHOD,
-                     NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL,
-                     NULL);
-        nm_connection_add_setting(connection, setting);
-
-        setting = nm_setting_ip6_config_new();
-        g_object_set(setting,
-                     NM_SETTING_IP_CONFIG_METHOD,
-                     NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL,
-                     NM_SETTING_IP_CONFIG_MAY_FAIL,
-                     TRUE,
-                     NULL);
-        nm_connection_add_setting(connection, setting);
-    }
-
     return connection;
 }
 
@@ -2043,14 +2017,20 @@ static const NMDBusInterfaceInfoExtended interface_info_device_wired = {
         NM_DBUS_INTERFACE_DEVICE_WIRED,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("PermHwAddress",
-                                                           "s",
-                                                           NM_DEVICE_PERM_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "PermHwAddress",
+                "s",
+                NM_DEVICE_PERM_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Speed", "u", NM_DEVICE_ETHERNET_SPEED),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("S390Subchannels",
                                                            "as",
                                                            NM_DEVICE_ETHERNET_S390_SUBCHANNELS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-factory.c b/src/core/devices/nm-device-factory.c
index 2f0ae2e8..c97fbb57 100644
--- a/src/core/devices/nm-device-factory.c
+++ b/src/core/devices/nm-device-factory.c
@@ -396,6 +396,7 @@ nm_device_factory_manager_load_factories(NMDeviceFactoryManagerFactoryFunc callb
     _ADD_INTERNAL(nm_bridge_device_factory_get_type);
     _ADD_INTERNAL(nm_dummy_device_factory_get_type);
     _ADD_INTERNAL(nm_ethernet_device_factory_get_type);
+    _ADD_INTERNAL(nm_hsr_device_factory_get_type);
     _ADD_INTERNAL(nm_infiniband_device_factory_get_type);
     _ADD_INTERNAL(nm_ip_tunnel_device_factory_get_type);
     _ADD_INTERNAL(nm_loopback_device_factory_get_type);
diff --git a/src/core/devices/nm-device-generic.c b/src/core/devices/nm-device-generic.c
index c0dcf0de..ead671d4 100644
--- a/src/core/devices/nm-device-generic.c
+++ b/src/core/devices/nm-device-generic.c
@@ -168,7 +168,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_generic = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_GENERIC,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
                 "TypeDescription",
                 "s",
diff --git a/src/core/devices/nm-device-hsr.c b/src/core/devices/nm-device-hsr.c
new file mode 100644
index 00000000..8156fa77
--- /dev/null
+++ b/src/core/devices/nm-device-hsr.c
@@ -0,0 +1,287 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-manager.h"
+#include "nm-device-hsr.h"
+
+#include <linux/if_ether.h>
+
+#include "libnm-core-intern/nm-core-internal.h"
+#include "nm-act-request.h"
+#include "nm-device-private.h"
+#include "nm-setting-hsr.h"
+#include "libnm-platform/nm-platform.h"
+#include "nm-device-factory.h"
+
+#define _NMLOG_DEVICE_TYPE NMDeviceHsr
+#include "nm-device-logging.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE(NMDeviceHsr,
+                             PROP_PORT1,
+                             PROP_PORT2,
+                             PROP_SUPERVISION_ADDRESS,
+                             PROP_MULTICAST_SPEC,
+                             PROP_PRP, );
+
+typedef struct {
+    NMPlatformLnkHsr props;
+} NMDeviceHsrPrivate;
+
+struct _NMDeviceHsr {
+    NMDevice           parent;
+    NMDeviceHsrPrivate _priv;
+};
+
+struct _NMDeviceHsrClass {
+    NMDeviceClass parent;
+};
+
+G_DEFINE_TYPE(NMDeviceHsr, nm_device_hsr, NM_TYPE_DEVICE)
+
+#define NM_DEVICE_HSR_GET_PRIVATE(self) \
+    _NM_GET_PRIVATE(self, NMDeviceHsr, NM_IS_DEVICE_HSR, NMDevice)
+
+/*****************************************************************************/
+
+static NMDeviceCapabilities
+get_generic_capabilities(NMDevice *dev)
+{
+    return NM_DEVICE_CAP_CARRIER_DETECT | NM_DEVICE_CAP_IS_SOFTWARE;
+}
+
+static void
+update_properties(NMDevice *device)
+{
+    NMDeviceHsr            *self;
+    NMDeviceHsrPrivate     *priv;
+    const NMPlatformLink   *plink;
+    const NMPlatformLnkHsr *props;
+    int                     ifindex;
+
+    g_return_if_fail(NM_IS_DEVICE_HSR(device));
+    self = NM_DEVICE_HSR(device);
+    priv = NM_DEVICE_HSR_GET_PRIVATE(self);
+
+    ifindex = nm_device_get_ifindex(device);
+    g_return_if_fail(ifindex > 0);
+    props = nm_platform_link_get_lnk_hsr(nm_device_get_platform(device), ifindex, &plink);
+
+    if (!props) {
+        _LOGW(LOGD_PLATFORM, "could not get HSR properties");
+        return;
+    }
+
+    g_object_freeze_notify((GObject *) device);
+
+#define CHECK_PROPERTY_CHANGED(field, prop)      \
+    G_STMT_START                                 \
+    {                                            \
+        if (priv->props.field != props->field) { \
+            priv->props.field = props->field;    \
+            _notify(self, prop);                 \
+        }                                        \
+    }                                            \
+    G_STMT_END
+
+    CHECK_PROPERTY_CHANGED(port1, PROP_PORT1);
+    CHECK_PROPERTY_CHANGED(port2, PROP_PORT2);
+    CHECK_PROPERTY_CHANGED(multicast_spec, PROP_MULTICAST_SPEC);
+    CHECK_PROPERTY_CHANGED(prp, PROP_PRP);
+
+    if (!nm_ether_addr_equal(&priv->props.supervision_address, &props->supervision_address))
+        _notify(self, PROP_SUPERVISION_ADDRESS);
+
+    g_object_thaw_notify((GObject *) device);
+}
+
+static void
+link_changed(NMDevice *device, const NMPlatformLink *pllink)
+{
+    NM_DEVICE_CLASS(nm_device_hsr_parent_class)->link_changed(device, pllink);
+    update_properties(device);
+}
+
+static gboolean
+create_and_realize(NMDevice              *device,
+                   NMConnection          *connection,
+                   NMDevice              *parent,
+                   const NMPlatformLink **out_plink,
+                   GError               **error)
+{
+    const char      *iface = nm_device_get_iface(device);
+    NMSettingHsr    *s_hsr;
+    NMPlatformLnkHsr lnk = {};
+    int              r;
+
+    s_hsr = _nm_connection_get_setting(connection, NM_TYPE_SETTING_HSR);
+    nm_assert(s_hsr);
+
+    if (nm_setting_hsr_get_port1(s_hsr) != NULL)
+        lnk.port1 = nm_platform_link_get_ifindex(NM_PLATFORM_GET, nm_setting_hsr_get_port1(s_hsr));
+    if (nm_setting_hsr_get_port2(s_hsr) != NULL)
+        lnk.port2 = nm_platform_link_get_ifindex(NM_PLATFORM_GET, nm_setting_hsr_get_port2(s_hsr));
+    lnk.multicast_spec = nm_setting_hsr_get_multicast_spec(s_hsr);
+    lnk.prp            = nm_setting_hsr_get_prp(s_hsr);
+    r = nm_platform_link_hsr_add(nm_device_get_platform(device), iface, &lnk, out_plink);
+    if (r < 0) {
+        g_set_error(error,
+                    NM_DEVICE_ERROR,
+                    NM_DEVICE_ERROR_CREATION_FAILED,
+                    "Failed to create HSR interface '%s' for '%s': %s",
+                    iface,
+                    nm_connection_get_id(connection),
+                    nm_strerror(r));
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+    NMDeviceHsr        *self = NM_DEVICE_HSR(object);
+    NMDeviceHsrPrivate *priv = NM_DEVICE_HSR_GET_PRIVATE(self);
+    NMDevice           *port;
+
+    switch (prop_id) {
+    case PROP_PORT1:
+        port = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, priv->props.port1);
+        nm_dbus_utils_g_value_set_object_path(value, port);
+        break;
+    case PROP_PORT2:
+        port = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, priv->props.port2);
+        nm_dbus_utils_g_value_set_object_path(value, port);
+        break;
+    case PROP_SUPERVISION_ADDRESS:
+        g_value_take_string(value, nm_ether_addr_to_string_dup(&priv->props.supervision_address));
+        break;
+    case PROP_MULTICAST_SPEC:
+        g_value_set_uchar(value, priv->props.multicast_spec);
+        break;
+    case PROP_PRP:
+        g_value_set_boolean(value, priv->props.prp);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+nm_device_hsr_init(NMDeviceHsr *self)
+{}
+
+/*****************************************************************************/
+
+static const NMDBusInterfaceInfoExtended interface_info_device_hsr = {
+    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
+        NM_DBUS_INTERFACE_DEVICE_HSR,
+        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Port1", "o", NM_DEVICE_HSR_PORT1),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Port2", "o", NM_DEVICE_HSR_PORT2),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("SupervisionAddress",
+                                                           "s",
+                                                           NM_DEVICE_HSR_SUPERVISION_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("MulticastSpec",
+                                                           "y",
+                                                           NM_DEVICE_HSR_MULTICAST_SPEC),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Prp", "b", NM_DEVICE_HSR_PRP), ), ),
+};
+
+static void
+nm_device_hsr_class_init(NMDeviceHsrClass *klass)
+{
+    GObjectClass      *object_class      = G_OBJECT_CLASS(klass);
+    NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(klass);
+    NMDeviceClass     *device_class      = NM_DEVICE_CLASS(klass);
+
+    object_class->get_property = get_property;
+
+    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_device_hsr);
+
+    device_class->connection_type_supported        = NM_SETTING_HSR_SETTING_NAME;
+    device_class->connection_type_check_compatible = NM_SETTING_HSR_SETTING_NAME;
+    device_class->link_types                       = NM_DEVICE_DEFINE_LINK_TYPES(NM_LINK_TYPE_HSR);
+
+    device_class->link_changed             = link_changed;
+    device_class->create_and_realize       = create_and_realize;
+    device_class->get_generic_capabilities = get_generic_capabilities;
+
+    obj_properties[PROP_PORT1] = g_param_spec_string(NM_DEVICE_HSR_PORT1,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PORT2] = g_param_spec_string(NM_DEVICE_HSR_PORT2,
+                                                     "",
+                                                     "",
+                                                     NULL,
+                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_SUPERVISION_ADDRESS] =
+        g_param_spec_string(NM_DEVICE_HSR_SUPERVISION_ADDRESS,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_MULTICAST_SPEC] =
+        g_param_spec_uchar(NM_DEVICE_HSR_MULTICAST_SPEC,
+                           "",
+                           "",
+                           0,
+                           G_MAXUINT8,
+                           0,
+                           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    obj_properties[PROP_PRP] = g_param_spec_boolean(NM_DEVICE_HSR_PRP,
+                                                    "",
+                                                    "",
+                                                    FALSE,
+                                                    G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
+
+/*****************************************************************************/
+
+#define NM_TYPE_HSR_DEVICE_FACTORY (nm_hsr_device_factory_get_type())
+#define NM_HSR_DEVICE_FACTORY(obj) \
+    (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_HSR_DEVICE_FACTORY, NMHsrDeviceFactory))
+
+static NMDevice *
+create_device(NMDeviceFactory      *factory,
+              const char           *iface,
+              const NMPlatformLink *plink,
+              NMConnection         *connection,
+              gboolean             *out_ignore)
+{
+    return g_object_new(NM_TYPE_DEVICE_HSR,
+                        NM_DEVICE_IFACE,
+                        iface,
+                        NM_DEVICE_TYPE_DESC,
+                        "hsr",
+                        NM_DEVICE_DEVICE_TYPE,
+                        NM_DEVICE_TYPE_HSR,
+                        NM_DEVICE_LINK_TYPE,
+                        NM_LINK_TYPE_HSR,
+                        NULL);
+}
+
+NM_DEVICE_FACTORY_DEFINE_INTERNAL(
+    HSR,
+    Hsr,
+    hsr,
+    NM_DEVICE_FACTORY_DECLARE_LINK_TYPES(NM_LINK_TYPE_HSR)
+        NM_DEVICE_FACTORY_DECLARE_SETTING_TYPES(NM_SETTING_HSR_SETTING_NAME),
+    factory_class->create_device = create_device;)
diff --git a/src/core/devices/nm-device-hsr.h b/src/core/devices/nm-device-hsr.h
new file mode 100644
index 00000000..1da90358
--- /dev/null
+++ b/src/core/devices/nm-device-hsr.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ */
+
+#ifndef __NETWORKMANAGER_DEVICE_HSR_H__
+#define __NETWORKMANAGER_DEVICE_HSR_H__
+
+#include "nm-device.h"
+
+#define NM_TYPE_DEVICE_HSR (nm_device_hsr_get_type())
+#define NM_DEVICE_HSR(obj) (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_DEVICE_HSR, NMDeviceHsr))
+#define NM_DEVICE_HSR_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_DEVICE_HSR, NMDeviceHsrClass))
+#define NM_IS_DEVICE_HSR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_DEVICE_HSR))
+#define NM_IS_DEVICE_HSR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_DEVICE_HSR))
+#define NM_DEVICE_HSR_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DEVICE_HSR, NMDeviceHsrClass))
+
+#define NM_DEVICE_HSR_PORT1               "port1"
+#define NM_DEVICE_HSR_PORT2               "port2"
+#define NM_DEVICE_HSR_SUPERVISION_ADDRESS "supervision-address"
+#define NM_DEVICE_HSR_MULTICAST_SPEC      "multicast-spec"
+#define NM_DEVICE_HSR_PRP                 "prp"
+
+typedef struct _NMDeviceHsr      NMDeviceHsr;
+typedef struct _NMDeviceHsrClass NMDeviceHsrClass;
+
+GType nm_device_hsr_get_type(void);
+
+#endif /* __NETWORKMANAGER_DEVICE_HSR_H__ */
diff --git a/src/core/devices/nm-device-infiniband.c b/src/core/devices/nm-device-infiniband.c
index 6ce61d0b..c974696c 100644
--- a/src/core/devices/nm-device-infiniband.c
+++ b/src/core/devices/nm-device-infiniband.c
@@ -362,8 +362,16 @@ static const NMDBusInterfaceInfoExtended interface_info_device_infiniband = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_INFINIBAND,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-tun.c b/src/core/devices/nm-device-tun.c
index 430e62aa..28b03cec 100644
--- a/src/core/devices/nm-device-tun.c
+++ b/src/core/devices/nm-device-tun.c
@@ -452,9 +452,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_tun = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("MultiQueue",
                                                            "b",
                                                            NM_DEVICE_TUN_MULTI_QUEUE),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
-                                                           "s",
-                                                           NM_DEVICE_HW_ADDRESS), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device-vlan.c b/src/core/devices/nm-device-vlan.c
index 7849e724..cc649ec9 100644
--- a/src/core/devices/nm-device-vlan.c
+++ b/src/core/devices/nm-device-vlan.c
@@ -525,8 +525,16 @@ static const NMDBusInterfaceInfoExtended interface_info_device_vlan = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_VLAN,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Parent", "o", NM_DEVICE_PARENT),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("VlanId", "u", NM_DEVICE_VLAN_ID), ), ),
 };
diff --git a/src/core/devices/nm-device-vxlan.c b/src/core/devices/nm-device-vxlan.c
index 061ee3f2..6a23d51e 100644
--- a/src/core/devices/nm-device-vxlan.c
+++ b/src/core/devices/nm-device-vxlan.c
@@ -576,7 +576,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_vxlan = {
         NM_DBUS_INTERFACE_DEVICE_VXLAN,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Parent", "o", NM_DEVICE_PARENT),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Id", "u", NM_DEVICE_VXLAN_ID),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Group", "s", NM_DEVICE_VXLAN_GROUP),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Local", "s", NM_DEVICE_VXLAN_LOCAL),
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index db02033d..dd40d11e 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -598,7 +598,7 @@ _peers_add(NMDeviceWireGuard *self, NMWireGuardPeer *peer)
     };
 
     c_list_link_tail(&priv->lst_peers_head, &peer_data->lst_peers);
-    if (!nm_g_hash_table_add(priv->peers, peer_data))
+    if (!g_hash_table_add(priv->peers, peer_data))
         nm_assert_not_reached();
     return peer_data;
 }
@@ -720,7 +720,7 @@ _peers_retry_in_msec(PeerData *peer_data, gboolean after_failure)
         return RETRY_IN_MSEC_MAX;
 
     /* double the retry-time, starting with one second. */
-    return NM_MIN(RETRY_IN_MSEC_MAX, (1u << peer_data->ep_resolv.resolv_fail_count) * 500);
+    return NM_MIN(RETRY_IN_MSEC_MAX, (1l << peer_data->ep_resolv.resolv_fail_count) * 500);
 }
 
 static void
diff --git a/src/core/devices/nm-device-wpan.c b/src/core/devices/nm-device-wpan.c
index 282eea87..7885355d 100644
--- a/src/core/devices/nm-device-wpan.c
+++ b/src/core/devices/nm-device-wpan.c
@@ -197,9 +197,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_wpan = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_WPAN,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress",
-                                                           "s",
-                                                           NM_DEVICE_HW_ADDRESS), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index d1212560..16f8e1f2 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -22,6 +22,7 @@
 #include <linux/rtnetlink.h>
 #include <linux/if_ether.h>
 #include <linux/if_infiniband.h>
+#include <libudev.h>
 
 #include "libnm-std-aux/unaligned.h"
 #include "libnm-glib-aux/nm-uuid.h"
@@ -303,6 +304,8 @@ typedef struct {
     NMEthtoolCoalesceState *coalesce;
     NMEthtoolRingState     *ring;
     NMEthtoolPauseState    *pause;
+    NMEthtoolChannelsState *channels;
+    NMEthtoolEEEState      *eee;
 } EthtoolState;
 
 typedef enum {
@@ -1030,12 +1033,14 @@ _prop_get_connection_stable_id(NMDevice          *self,
         gs_free char        *generated = NULL;
         NMUtilsStableType    stable_type;
         NMSettingConnection *s_con;
+        NMSettingWireless   *s_wifi;
         gboolean             hwaddr_is_fake;
         const char          *hwaddr;
         const char          *stable_id;
         const char          *uuid;
 
-        s_con = nm_connection_get_setting_connection(connection);
+        s_con  = nm_connection_get_setting_connection(connection);
+        s_wifi = nm_connection_get_setting_wireless(connection);
 
         stable_id = nm_setting_connection_get_stable_id(s_con);
 
@@ -1058,6 +1063,7 @@ _prop_get_connection_stable_id(NMDevice          *self,
                                                !hwaddr_is_fake ? hwaddr : NULL,
                                                nm_utils_boot_id_str(),
                                                uuid,
+                                               s_wifi ? nm_setting_wireless_get_ssid(s_wifi) : NULL,
                                                &generated);
 
         /* current_stable_id_type is a bitfield! */
@@ -1517,7 +1523,7 @@ _prop_get_ipvx_route_table(NMDevice *self, int addr_family)
 
     if (route_table == 0u && connection
         && (s_con = nm_connection_get_setting_connection(connection))
-        && (nm_streq0(nm_setting_connection_get_slave_type(s_con), NM_SETTING_VRF_SETTING_NAME)
+        && (nm_streq0(nm_setting_connection_get_port_type(s_con), NM_SETTING_VRF_SETTING_NAME)
             && priv->master && nm_device_get_device_type(priv->master) == NM_DEVICE_TYPE_VRF)) {
         const NMPlatformLnkVrf *lnk;
 
@@ -1655,7 +1661,7 @@ _prop_get_ipv4_dad_timeout(NMDevice *self)
                                                        self,
                                                        0,
                                                        NM_SETTING_IP_CONFIG_DAD_TIMEOUT_MAX,
-                                                       0);
+                                                       200);
 }
 
 static guint32
@@ -2017,7 +2023,10 @@ _prop_get_connection_mud_url(NMDevice *self, NMSettingConnection *s_con)
 }
 
 static GBytes *
-_prop_get_ipv4_dhcp_client_id(NMDevice *self, NMConnection *connection, GBytes *hwaddr)
+_prop_get_ipv4_dhcp_client_id(NMDevice     *self,
+                              NMConnection *connection,
+                              GBytes       *hwaddr,
+                              gboolean     *out_send_client_id)
 {
     NMSettingIPConfig *s_ip4;
     const char        *client_id;
@@ -2030,6 +2039,8 @@ _prop_get_ipv4_dhcp_client_id(NMDevice *self, NMConnection *connection, GBytes *
     GBytes            *result;
     gs_free char      *logstr1 = NULL;
 
+    NM_SET_OUT(out_send_client_id, TRUE);
+
     s_ip4     = nm_connection_get_setting_ip4_config(connection);
     client_id = nm_setting_ip4_config_get_dhcp_client_id(NM_SETTING_IP4_CONFIG(s_ip4));
 
@@ -2049,6 +2060,12 @@ _prop_get_ipv4_dhcp_client_id(NMDevice *self, NMConnection *connection, GBytes *
         return NULL;
     }
 
+    if (nm_streq(client_id, "none")) {
+        _LOGD(LOGD_DEVICE | LOGD_DHCP4 | LOGD_IP4, "ipv4.dhcp-client-id: set to \"none\"");
+        NM_SET_OUT(out_send_client_id, FALSE);
+        return NULL;
+    }
+
     if (nm_streq(client_id, "mac")) {
         if (!hwaddr) {
             fail_reason = "missing link-layer address";
@@ -2335,7 +2352,7 @@ _prop_get_x_cloned_mac_address(NMDevice *self, NMConnection *connection, gboolea
                 if (v == NM_SETTING_MAC_RANDOMIZATION_ALWAYS)
                     addr = NM_CLONED_MAC_RANDOM;
             }
-        } else if (NM_CLONED_MAC_IS_SPECIAL(a) || nm_utils_hwaddr_valid(a, ETH_ALEN))
+        } else if (NM_CLONED_MAC_IS_SPECIAL(a, is_wifi) || nm_utils_hwaddr_valid(a, ETH_ALEN))
             addr = a;
     }
 
@@ -2374,6 +2391,8 @@ _ethtool_features_reset(NMDevice *self, NMPlatform *platform, EthtoolState *etht
     gs_free NMEthtoolFeatureStates *features = NULL;
 
     features = g_steal_pointer(&ethtool_state->features);
+    if (!features)
+        return;
 
     if (!nm_platform_ethtool_set_features(platform,
                                           ethtool_state->ifindex,
@@ -2393,8 +2412,7 @@ _ethtool_features_set(NMDevice         *self,
 {
     gs_free NMEthtoolFeatureStates *features = NULL;
 
-    if (ethtool_state->features)
-        _ethtool_features_reset(self, platform, ethtool_state);
+    _ethtool_features_reset(self, platform, ethtool_state);
 
     if (nm_setting_ethtool_init_features(s_ethtool, ethtool_state->requested) == 0)
         return;
@@ -2592,6 +2610,104 @@ _ethtool_ring_set(NMDevice         *self,
 }
 
 static void
+_ethtool_channels_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
+{
+    gs_free NMEthtoolChannelsState *channels = NULL;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(ethtool_state);
+
+    channels = g_steal_pointer(&ethtool_state->channels);
+    if (!channels)
+        return;
+
+    if (!nm_platform_ethtool_set_channels(platform, ethtool_state->ifindex, channels))
+        _LOGW(LOGD_DEVICE, "ethtool: failure resetting one or more channels settings");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: channels settings successfully reset");
+}
+
+static void
+_ethtool_channels_set(NMDevice         *self,
+                      NMPlatform       *platform,
+                      EthtoolState     *ethtool_state,
+                      NMSettingEthtool *s_ethtool)
+{
+    NMEthtoolChannelsState channels_old;
+    NMEthtoolChannelsState channels_new;
+    GHashTable            *hash;
+    GHashTableIter         iter;
+    const char            *name;
+    GVariant              *variant;
+    gboolean               has_old = FALSE;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool));
+    nm_assert(ethtool_state);
+    nm_assert(!ethtool_state->channels);
+
+    hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE);
+    if (!hash)
+        return;
+
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) {
+        NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name);
+        guint32     u32;
+
+        if (!nm_ethtool_id_is_channels(ethtool_id))
+            continue;
+
+        nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_UINT32));
+
+        if (!has_old) {
+            if (!nm_platform_ethtool_get_link_channels(platform,
+                                                       ethtool_state->ifindex,
+                                                       &channels_old)) {
+                _LOGW(LOGD_DEVICE,
+                      "ethtool: failure setting channels options (cannot read existing setting)");
+                return;
+            }
+            has_old      = TRUE;
+            channels_new = channels_old;
+        }
+
+        u32 = g_variant_get_uint32(variant);
+
+        switch (ethtool_id) {
+        case NM_ETHTOOL_ID_CHANNELS_RX:
+            channels_new.rx = u32;
+            break;
+        case NM_ETHTOOL_ID_CHANNELS_TX:
+            channels_new.tx = u32;
+            break;
+        case NM_ETHTOOL_ID_CHANNELS_OTHER:
+            channels_new.other = u32;
+            break;
+        case NM_ETHTOOL_ID_CHANNELS_COMBINED:
+            channels_new.combined = u32;
+            break;
+        default:
+            nm_assert_not_reached();
+        }
+    }
+
+    if (!has_old)
+        return;
+
+    ethtool_state->channels = nm_memdup(&channels_old, sizeof(channels_old));
+
+    if (!nm_platform_ethtool_set_channels(platform, ethtool_state->ifindex, &channels_new)) {
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting channels settings");
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "ethtool: channels settings successfully set");
+}
+
+static void
 _ethtool_pause_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
 {
     gs_free NMEthtoolPauseState *pause = NULL;
@@ -2611,6 +2727,25 @@ _ethtool_pause_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool
 }
 
 static void
+_ethtool_eee_reset(NMDevice *self, NMPlatform *platform, EthtoolState *ethtool_state)
+{
+    gs_free NMEthtoolEEEState *eee = NULL;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(ethtool_state);
+
+    eee = g_steal_pointer(&ethtool_state->eee);
+    if (!eee)
+        return;
+
+    if (!nm_platform_ethtool_set_eee(platform, ethtool_state->ifindex, eee))
+        _LOGW(LOGD_DEVICE, "ethtool: failure resetting eee settings");
+    else
+        _LOGD(LOGD_DEVICE, "ethtool: eee settings successfully reset");
+}
+
+static void
 _ethtool_pause_set(NMDevice         *self,
                    NMPlatform       *platform,
                    EthtoolState     *ethtool_state,
@@ -2699,6 +2834,73 @@ _ethtool_pause_set(NMDevice         *self,
 }
 
 static void
+_ethtool_eee_set(NMDevice         *self,
+                 NMPlatform       *platform,
+                 EthtoolState     *ethtool_state,
+                 NMSettingEthtool *s_ethtool)
+{
+    NMEthtoolEEEState eee_old;
+    NMEthtoolEEEState eee_new;
+    GHashTable       *hash;
+    GHashTableIter    iter;
+    const char       *name;
+    GVariant         *variant;
+    gboolean          has_old = FALSE;
+    NMTernary         eee     = NM_TERNARY_DEFAULT;
+
+    nm_assert(NM_IS_DEVICE(self));
+    nm_assert(NM_IS_PLATFORM(platform));
+    nm_assert(NM_IS_SETTING_ETHTOOL(s_ethtool));
+    nm_assert(ethtool_state);
+    nm_assert(!ethtool_state->eee);
+
+    hash = _nm_setting_option_hash(NM_SETTING(s_ethtool), FALSE);
+    if (!hash)
+        return;
+
+    g_hash_table_iter_init(&iter, hash);
+    while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &variant)) {
+        NMEthtoolID ethtool_id = nm_ethtool_id_get_by_name(name);
+
+        if (!nm_ethtool_id_is_eee(ethtool_id))
+            continue;
+
+        nm_assert(g_variant_is_of_type(variant, G_VARIANT_TYPE_BOOLEAN));
+
+        if (!has_old) {
+            if (!nm_platform_ethtool_get_link_eee(platform, ethtool_state->ifindex, &eee_old)) {
+                _LOGW(LOGD_DEVICE,
+                      "ethtool: failure setting eee options (cannot read "
+                      "existing setting)");
+                return;
+            }
+            has_old = TRUE;
+        }
+
+        if (ethtool_id == NM_ETHTOOL_ID_EEE_ENABLED)
+            eee = g_variant_get_boolean(variant);
+        else
+            nm_assert_not_reached();
+    }
+
+    if (!has_old)
+        return;
+
+    eee_new = eee_old;
+    if (eee != NM_TERNARY_DEFAULT)
+        eee_new.enabled = !!eee;
+
+    ethtool_state->eee = nm_memdup(&eee_old, sizeof(eee_old));
+
+    if (!nm_platform_ethtool_set_eee(platform, ethtool_state->ifindex, &eee_new)) {
+        _LOGW(LOGD_DEVICE, "ethtool: failure setting eee settings");
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "ethtool: eee settings successfully set");
+}
+
+static void
 _ethtool_state_reset(NMDevice *self)
 {
     NMPlatform           *platform      = nm_device_get_platform(self);
@@ -2708,14 +2910,12 @@ _ethtool_state_reset(NMDevice *self)
     if (!ethtool_state)
         return;
 
-    if (ethtool_state->features)
-        _ethtool_features_reset(self, platform, ethtool_state);
-    if (ethtool_state->coalesce)
-        _ethtool_coalesce_reset(self, platform, ethtool_state);
-    if (ethtool_state->ring)
-        _ethtool_ring_reset(self, platform, ethtool_state);
-    if (ethtool_state->pause)
-        _ethtool_pause_reset(self, platform, ethtool_state);
+    _ethtool_features_reset(self, platform, ethtool_state);
+    _ethtool_coalesce_reset(self, platform, ethtool_state);
+    _ethtool_ring_reset(self, platform, ethtool_state);
+    _ethtool_pause_reset(self, platform, ethtool_state);
+    _ethtool_channels_reset(self, platform, ethtool_state);
+    _ethtool_eee_reset(self, platform, ethtool_state);
 }
 
 static void
@@ -2750,9 +2950,11 @@ _ethtool_state_set(NMDevice *self)
     _ethtool_coalesce_set(self, platform, ethtool_state, s_ethtool);
     _ethtool_ring_set(self, platform, ethtool_state, s_ethtool);
     _ethtool_pause_set(self, platform, ethtool_state, s_ethtool);
+    _ethtool_channels_set(self, platform, ethtool_state, s_ethtool);
+    _ethtool_eee_set(self, platform, ethtool_state, s_ethtool);
 
     if (ethtool_state->features || ethtool_state->coalesce || ethtool_state->ring
-        || ethtool_state->pause)
+        || ethtool_state->pause || ethtool_state->channels || ethtool_state->eee)
         priv->ethtool_state = g_steal_pointer(&ethtool_state);
 }
 
@@ -2863,7 +3065,7 @@ nm_device_link_properties_set(NMDevice *self, gboolean reapply)
     _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)) {
+    if (nm_platform_link_change(platform, ifindex, &props, NULL, NULL, flags)) {
         _LOGD(LOGD_DEVICE, "link properties successfully set");
     } else {
         _LOGW(LOGD_DEVICE, "failure setting link properties");
@@ -2891,6 +3093,7 @@ link_properties_reset(NMDevice *self)
                                 ifindex,
                                 &priv->link_props_state.props,
                                 NULL,
+                                NULL,
                                 priv->link_props_state.flags)) {
         _LOGD(LOGD_DEVICE, "link properties successfully reset");
     } else {
@@ -4228,6 +4431,19 @@ update_external_connection(NMDevice *self)
 }
 
 static void
+_dev_ipv6_log_conflicts(NMDevice *self, GArray *conflicts)
+{
+    guint i;
+    char  sbuf[NM_INET_ADDRSTRLEN];
+
+    for (i = 0; i < conflicts->len; i++) {
+        const struct in6_addr *addr = &nm_g_array_index(conflicts, const struct in6_addr, i);
+
+        _LOGI(LOGD_DEVICE, "Conflict detected for IPv6 address: %s", nm_inet6_ntop(addr, sbuf));
+    }
+}
+
+static void
 _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
@@ -4250,7 +4466,7 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         char                   buf_addr[NM_INET_ADDRSTRLEN];
 
         if (addr_info->state == NM_L3_ACD_ADDR_STATE_USED) {
-            _LOGI(LOGD_DEVICE,
+            _LOGW(LOGD_DEVICE,
                   "IP address %s cannot be configured because it is already in use in the "
                   "network by host %s",
                   nm_inet4_ntop(addr_info->addr, buf_addr),
@@ -4284,7 +4500,6 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
         if (priv->ipshared_data_4.state == NM_DEVICE_IP_STATE_PENDING
             && !priv->ipshared_data_4.v4.dnsmasq_manager && priv->ipshared_data_4.v4.l3cd) {
             _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);
@@ -4322,6 +4537,10 @@ _dev_l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, N
                                          NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
                                          &conflicts);
             if (conflicts) {
+                if (_NMLOG_ENABLED(LOGL_INFO, LOGD_DEVICE)) {
+                    _dev_ipv6_log_conflicts(self, conflicts);
+                }
+
                 /* nm_ndisc_dad_failed() will emit a new "NDisc:config-received"
                  * signal; _dev_ipac6_ndisc_config_changed() will be called
                  * synchronously to update the current state and schedule a commit. */
@@ -5231,6 +5450,8 @@ nm_device_get_route_metric_default(NMDeviceType device_type)
         return 200;
     case NM_DEVICE_TYPE_WIMAX:
         return 250;
+    case NM_DEVICE_TYPE_HSR:
+        return 275;
     case NM_DEVICE_TYPE_BOND:
         return 300;
     case NM_DEVICE_TYPE_TEAM:
@@ -5715,7 +5936,7 @@ out:
     return FALSE;
 }
 
-#define CONCHECK_P_PROBE_INTERVAL 1
+#define CONCHECK_P_PROBE_INTERVAL 1u
 
 static void
 concheck_periodic_schedule_set(NMDevice *self, int addr_family, ConcheckScheduleMode mode)
@@ -5887,7 +6108,7 @@ concheck_update_interval(NMDevice *self, int addr_family, gboolean check_now)
 
     new_interval = nm_connectivity_get_interval(concheck_get_mgr(self));
 
-    new_interval = NM_MIN(new_interval, 7 * 24 * 3600);
+    new_interval = NM_MIN(new_interval, 7u * 24u * 3600u);
 
     if (new_interval != priv->concheck_x[IS_IPv4].p_max_interval) {
         _LOGT(LOGD_CONCHECK,
@@ -7133,7 +7354,7 @@ device_ip_link_changed(gpointer user_data)
     ip_iface = pllink->name;
 
     if (!ip_iface[0])
-        return FALSE;
+        return G_SOURCE_REMOVE;
 
     if (!nm_streq(priv->ip_iface, ip_iface)) {
         _LOGI(LOGD_DEVICE,
@@ -7989,6 +8210,40 @@ nm_device_owns_iface(NMDevice *self, const char *iface)
     return FALSE;
 }
 
+static void
+apply_udev_auto_default_configs(NMDevice *self, NMConnection *connection)
+{
+    struct udev_device *dev;
+    const char         *uprop;
+    NMSetting          *setting;
+
+    dev = nm_platform_link_get_udev_device(nm_device_get_platform(NM_DEVICE(self)),
+                                           nm_device_get_ip_ifindex(self));
+    if (!dev)
+        return;
+
+    uprop = udev_device_get_property_value(dev, "NM_AUTO_DEFAULT_LINK_LOCAL_ONLY");
+    uprop = uprop ?: udev_device_get_property_value(dev, "ID_NET_AUTO_LINK_LOCAL_ONLY");
+
+    if (_nm_utils_ascii_str_to_bool(uprop, FALSE)) {
+        setting = nm_setting_ip4_config_new();
+        g_object_set(setting,
+                     NM_SETTING_IP_CONFIG_METHOD,
+                     NM_SETTING_IP4_CONFIG_METHOD_LINK_LOCAL,
+                     NULL);
+        nm_connection_add_setting(connection, setting);
+
+        setting = nm_setting_ip6_config_new();
+        g_object_set(setting,
+                     NM_SETTING_IP_CONFIG_METHOD,
+                     NM_SETTING_IP6_CONFIG_METHOD_LINK_LOCAL,
+                     NM_SETTING_IP_CONFIG_MAY_FAIL,
+                     TRUE,
+                     NULL);
+        nm_connection_add_setting(connection, setting);
+    }
+}
+
 NMConnection *
 nm_device_new_default_connection(NMDevice *self)
 {
@@ -8002,6 +8257,8 @@ nm_device_new_default_connection(NMDevice *self)
     if (!connection)
         return NULL;
 
+    apply_udev_auto_default_configs(self, connection);
+
     if (!nm_connection_normalize(connection, NULL, NULL, &error)) {
         _LOGD(LOGD_DEVICE, "device generated an invalid default connection: %s", error->message);
         g_error_free(error);
@@ -8323,7 +8580,7 @@ nm_device_slave_notify_release(NMDevice           *self,
             master_status = "failed";
             break;
         case NM_DEVICE_STATE_REASON_USER_REQUESTED:
-            reason        = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED;
+            reason        = NM_DEVICE_STATE_REASON_USER_REQUESTED;
             master_status = "deactivated by user request";
             break;
         case NM_DEVICE_STATE_REASON_CONNECTION_REMOVED:
@@ -9184,7 +9441,7 @@ nm_device_check_slave_connection_compatible(NMDevice *self, NMConnection *slave)
 
     s_con = nm_connection_get_setting_connection(slave);
     g_assert(s_con);
-    slave_type = nm_setting_connection_get_slave_type(s_con);
+    slave_type = nm_setting_connection_get_port_type(s_con);
     if (!slave_type)
         return FALSE;
 
@@ -9746,10 +10003,16 @@ activate_stage1_device_prepare(NMDevice *self)
     master = nm_active_connection_get_master(active);
     if (master) {
         if (nm_active_connection_get_state(master) >= NM_ACTIVE_CONNECTION_STATE_DEACTIVATING) {
+            NMDevice           *master_device  = nm_active_connection_get_device(master);
+            NMDeviceStateReason failure_reason = NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED;
+
             _LOGD(LOGD_DEVICE, "master connection is deactivating");
-            nm_device_state_changed(self,
-                                    NM_DEVICE_STATE_FAILED,
-                                    NM_DEVICE_STATE_REASON_DEPENDENCY_FAILED);
+
+            if (master_device && NM_DEVICE_GET_PRIVATE(master_device)->queued_act_request) {
+                /* if the controller is going to activate again, don't block this device */
+                failure_reason = NM_DEVICE_STATE_REASON_NONE;
+            }
+            nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, failure_reason);
             return;
         }
         /* If the master connection is ready for slaves, attach ourselves */
@@ -10363,7 +10626,6 @@ _dev_ipmanual_check_ready(NMDevice *self)
     gboolean               has_carrier;
     NML3CfgCheckReadyFlags flags;
     gboolean               ready;
-    gs_unref_array GArray *conflicts = NULL;
     int                    IS_IPv4;
 
     if (priv->ipmanual_data.state_4 != NM_DEVICE_IP_STATE_PENDING
@@ -10393,26 +10655,41 @@ _dev_ipmanual_check_ready(NMDevice *self)
         }
     }
 
-    flags = NM_L3CFG_CHECK_READY_FLAGS_NONE;
-    if (has_carrier) {
-        flags |= NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY;
-        flags |= NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY;
-    }
+    flags = NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY;
+    flags |= NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY;
 
     for (IS_IPv4 = 0; IS_IPv4 < 2; IS_IPv4++) {
-        const int addr_family = IS_IPv4 ? AF_INET : AF_INET6;
+        const int              addr_family = IS_IPv4 ? AF_INET : AF_INET6;
+        gs_unref_array GArray *conflicts   = NULL;
 
         ready = nm_l3cfg_check_ready(priv->l3cfg,
                                      priv->l3cds[L3_CONFIG_DATA_TYPE_MANUALIP].d,
                                      addr_family,
                                      flags,
                                      &conflicts);
-        if (conflicts) {
-            _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_FAILED);
-            _dev_ip_state_check_async(self, AF_UNSPEC);
-        } else if (ready) {
-            _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_READY);
-            _dev_ip_state_check_async(self, AF_UNSPEC);
+
+        if (_NMLOG_ENABLED(LOGL_INFO, LOGD_DEVICE) && conflicts && !IS_IPv4) {
+            _dev_ipv6_log_conflicts(self, conflicts);
+        }
+
+        if (ready) {
+            guint num_addrs = 0;
+
+            num_addrs =
+                nm_l3_config_data_get_num_addresses(priv->l3cds[L3_CONFIG_DATA_TYPE_MANUALIP].d,
+                                                    addr_family);
+
+            if (conflicts && conflicts->len == num_addrs) {
+                _LOGD_ipmanual(addr_family, "all manual addresses failed DAD, failing");
+                _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_FAILED);
+                _dev_ip_state_check_async(self, AF_UNSPEC);
+            } else {
+                if (conflicts) {
+                    _LOGD_ipmanual(addr_family, "some manual addresses passed DAD, continuing");
+                }
+                _dev_ipmanual_set_state(self, addr_family, NM_DEVICE_IP_STATE_READY);
+                _dev_ip_state_check_async(self, AF_UNSPEC);
+            }
         }
     }
 }
@@ -10692,8 +10969,10 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
         const char *const     *reject_servers;
         const char            *hostname;
         gboolean               hostname_is_fqdn;
+        gboolean               send_client_id;
+
+        client_id = _prop_get_ipv4_dhcp_client_id(self, connection, hwaddr, &send_client_id);
 
-        client_id = _prop_get_ipv4_dhcp_client_id(self, connection, hwaddr);
         vendor_class_identifier =
             _prop_get_ipv4_dhcp_vendor_class_identifier(self, NM_SETTING_IP4_CONFIG(s_ip));
         reject_servers = nm_setting_ip_config_get_dhcp_reject_servers(s_ip, NULL);
@@ -10730,6 +11009,7 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
                 {
                     .request_broadcast = request_broadcast,
                     .acd_timeout_msec  = _prop_get_ipv4_dad_timeout(self),
+                    .send_client_id    = send_client_id,
                 },
             .previous_lease = priv->l3cds[L3_CONFIG_DATA_TYPE_DHCP_X(IS_IPv4)].d,
         };
@@ -11560,13 +11840,13 @@ _commit_mtu(NMDevice *self)
     mtu_plat = nm_platform_link_get_mtu(nm_device_get_platform(self), ifindex);
 
     if (ip6_mtu) {
-        ip6_mtu = NM_MAX(1280, ip6_mtu);
+        ip6_mtu = NM_MAX(1280u, ip6_mtu);
 
         if (!mtu_desired)
             mtu_desired = mtu_plat;
 
         if (mtu_desired) {
-            mtu_desired = NM_MAX(1280, mtu_desired);
+            mtu_desired = NM_MAX(1280u, mtu_desired);
 
             if (mtu_desired < ip6_mtu)
                 ip6_mtu = mtu_desired;
@@ -11736,8 +12016,9 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                 const NML3ConfigData *l3cd,
                                 NMDevice             *self)
 {
-    NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
-    gboolean         ready;
+    NMDevicePrivate       *priv = NM_DEVICE_GET_PRIVATE(self);
+    gboolean               ready;
+    gs_unref_array GArray *conflicts = NULL;
 
     /* The ndisc configuration changes when we receive a new RA or
      * when a lifetime expires; but also when DAD fails for a
@@ -11754,7 +12035,12 @@ _dev_ipac6_ndisc_config_changed(NMNDisc              *ndisc,
                                  l3cd,
                                  AF_INET6,
                                  NM_L3CFG_CHECK_READY_FLAGS_IP6_DAD_READY,
-                                 NULL);
+                                 &conflicts);
+
+    if (_NMLOG_ENABLED(LOGL_INFO, LOGD_DEVICE) && conflicts) {
+        _dev_ipv6_log_conflicts(self, conflicts);
+    }
+
     if (ready) {
         _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
     } else {
@@ -11856,16 +12142,8 @@ _dev_ipac6_start(NMDevice *self)
     NMUtilsIPv6IfaceId  iid;
     gboolean            is_token;
 
-    if (priv->ipac6_data.state == NM_DEVICE_IP_STATE_NONE) {
-        if (!g_file_test("/proc/sys/net/ipv6", G_FILE_TEST_IS_DIR)) {
-            _LOGI_ipac6("addrconf6: kernel does not support IPv6");
-            _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_FAILED);
-            _dev_ip_state_check_async(self, AF_INET6);
-            return;
-        }
-
+    if (priv->ipac6_data.state == NM_DEVICE_IP_STATE_NONE)
         _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_PENDING);
-    }
 
     if (NM_IN_SET(priv->ipll_data_6.state, NM_DEVICE_IP_STATE_NONE, NM_DEVICE_IP_STATE_PENDING)) {
         _dev_ipac6_grace_period_start(self, 30, TRUE);
@@ -12406,6 +12684,30 @@ activate_stage3_ip_config(NMDevice *self)
 
     ifindex = nm_device_get_ip_ifindex(self);
 
+    /* Add the interface to the specified firewall zone */
+    switch (priv->fw_state) {
+    case FIREWALL_STATE_UNMANAGED:
+        if (nm_device_sys_iface_state_is_external(self)) {
+            /* fake success */
+            priv->fw_state = FIREWALL_STATE_INITIALIZED;
+        } else if (ifindex > 0) {
+            priv->fw_state = FIREWALL_STATE_WAIT_STAGE_3;
+            fw_change_zone(self);
+            return;
+        }
+        /* no ifindex, nothing to do for now */
+        break;
+    case FIREWALL_STATE_WAIT_STAGE_3:
+        /* a firewall call for stage3 is pending. Return and wait. */
+        return;
+    default:
+        nm_assert(NM_IN_SET((FirewallState) priv->fw_state,
+                            FIREWALL_STATE_INITIALIZED,
+                            FIREWALL_STATE_WAIT_IP_CONFIG));
+        break;
+    }
+    nm_assert(ifindex <= 0 || priv->fw_state == FIREWALL_STATE_INITIALIZED);
+
     ipv4_method = nm_device_get_effective_ip_config_method(self, AF_INET);
     if (nm_streq(ipv4_method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
         /* "auto" usually means DHCPv4 or autoconf6, but it doesn't have to be. Subclasses
@@ -12414,8 +12716,18 @@ activate_stage3_ip_config(NMDevice *self)
     }
 
     ipv6_method = nm_device_get_effective_ip_config_method(self, AF_INET6);
-
-    if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
+    if (!g_file_test("/proc/sys/net/ipv6", G_FILE_TEST_IS_DIR)) {
+        _NMLOG_ip((nm_device_sys_iface_state_is_external(self)
+                   || NM_IN_STRSET(ipv6_method,
+                                   NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                                   NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
+                                   NM_SETTING_IP6_CONFIG_METHOD_IGNORE))
+                      ? LOGL_DEBUG
+                      : LOGL_WARN,
+                  AF_INET6,
+                  "IPv6 not supported by kernel resulting in \"ipv6.method=disabled\"");
+        ipv6_method = NM_SETTING_IP6_CONFIG_METHOD_DISABLED;
+    } else if (nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
         ipv6_method = klass->get_ip_method_auto(self, AF_INET6);
     }
 
@@ -12436,30 +12748,6 @@ activate_stage3_ip_config(NMDevice *self)
                         nm_streq(ipv6_method, NM_SETTING_IP6_CONFIG_METHOD_AUTO));
     }
 
-    /* Add the interface to the specified firewall zone */
-    switch (priv->fw_state) {
-    case FIREWALL_STATE_UNMANAGED:
-        if (nm_device_sys_iface_state_is_external(self)) {
-            /* fake success */
-            priv->fw_state = FIREWALL_STATE_INITIALIZED;
-        } else if (ifindex > 0) {
-            priv->fw_state = FIREWALL_STATE_WAIT_STAGE_3;
-            fw_change_zone(self);
-            return;
-        }
-        /* no ifindex, nothing to do for now */
-        break;
-    case FIREWALL_STATE_WAIT_STAGE_3:
-        /* a firewall call for stage3 is pending. Return and wait. */
-        return;
-    default:
-        nm_assert(NM_IN_SET((FirewallState) priv->fw_state,
-                            FIREWALL_STATE_INITIALIZED,
-                            FIREWALL_STATE_WAIT_IP_CONFIG));
-        break;
-    }
-    nm_assert(ifindex <= 0 || priv->fw_state == FIREWALL_STATE_INITIALIZED);
-
     if (priv->state < NM_DEVICE_STATE_IP_CONFIG) {
         _dev_ip_state_req_timeout_schedule(self, AF_INET);
         _dev_ip_state_req_timeout_schedule(self, AF_INET6);
@@ -12726,18 +13014,32 @@ out_fail:
 static void
 _dev_ipshared4_spawn_dnsmasq(NMDevice *self)
 {
-    NMDevicePrivate      *priv = NM_DEVICE_GET_PRIVATE(self);
-    const char           *ip_iface;
-    gs_free_error GError *error = NULL;
-    NMSettingConnection  *s_con;
-    gboolean              announce_android_metered;
-    NMConnection         *applied;
+    NMDevicePrivate       *priv = NM_DEVICE_GET_PRIVATE(self);
+    const char            *ip_iface;
+    gs_free_error GError  *error = NULL;
+    NMSettingConnection   *s_con;
+    gboolean               announce_android_metered;
+    NMConnection          *applied;
+    gs_unref_array GArray *conflicts = NULL;
+    gboolean               ready;
 
     nm_assert(priv->ipshared_data_4.v4.firewall_config);
     nm_assert(priv->ipshared_data_4.v4.dnsmasq_state_id == 0);
     nm_assert(!priv->ipshared_data_4.v4.dnsmasq_manager);
     nm_assert(priv->ipshared_data_4.v4.l3cd);
 
+    ready = nm_l3cfg_check_ready(priv->l3cfg,
+                                 priv->l3cds[L3_CONFIG_DATA_TYPE_SHARED_4].d,
+                                 AF_INET,
+                                 NM_L3CFG_CHECK_READY_FLAGS_IP4_ACD_READY,
+                                 &conflicts);
+    if (!ready) {
+        _LOGT_ipshared(AF_INET, "address not ready, wait");
+        return;
+    }
+    if (conflicts)
+        goto out_fail;
+
     ip_iface = nm_device_get_ip_iface(self);
     g_return_if_fail(ip_iface);
 
@@ -12782,9 +13084,11 @@ _dev_ipshared4_spawn_dnsmasq(NMDevice *self)
 
     _dev_ipsharedx_set_state(self, AF_INET, NM_DEVICE_IP_STATE_READY);
     _dev_ip_state_check_async(self, AF_INET);
+    nm_clear_l3cd(&priv->ipshared_data_4.v4.l3cd);
     return;
 
 out_fail:
+    nm_clear_l3cd(&priv->ipshared_data_4.v4.l3cd);
     _dev_ipsharedx_set_state(self, AF_INET, NM_DEVICE_IP_STATE_FAILED);
     _dev_ip_state_check_async(self, AF_INET);
 }
@@ -12993,7 +13297,7 @@ _nm_device_hash_check_invalid_keys(GHashTable        *hash,
 
         g_hash_table_iter_init(&iter, hash);
         while (g_hash_table_iter_next(&iter, (gpointer *) &k, NULL)) {
-            if (nm_strv_find_first(whitelist, -1, k) < 0) {
+            if (!nm_strv_contains(whitelist, -1, k)) {
                 first_invalid_key = k;
                 break;
             }
@@ -14952,20 +15256,23 @@ nm_device_set_unmanaged_by_user_settings(NMDevice *self, gboolean now)
 void
 nm_device_set_unmanaged_by_user_udev(NMDevice *self)
 {
-    int      ifindex;
-    gboolean platform_unmanaged = FALSE;
+    NMOptionBool platform_unmanaged;
+    int          ifindex;
 
     ifindex = self->_priv->ifindex;
 
-    if (ifindex <= 0
-        || !nm_platform_link_get_unmanaged(nm_device_get_platform(self),
-                                           ifindex,
-                                           &platform_unmanaged))
+    if (ifindex <= 0)
+        return;
+
+    platform_unmanaged = nm_platform_link_get_unmanaged(nm_device_get_platform(self), ifindex);
+    if (platform_unmanaged == NM_OPTION_BOOL_DEFAULT)
         return;
 
     nm_device_set_unmanaged_by_flags(self,
                                      NM_UNMANAGED_USER_UDEV,
-                                     platform_unmanaged,
+                                     platform_unmanaged == NM_OPTION_BOOL_TRUE
+                                         ? NM_UNMAN_FLAG_OP_SET_UNMANAGED
+                                         : NM_UNMAN_FLAG_OP_SET_MANAGED,
                                      NM_DEVICE_STATE_REASON_USER_REQUESTED);
 }
 
@@ -17128,9 +17435,12 @@ _hw_addr_get_cloned(NMDevice     *self,
 
         addr_out = g_steal_pointer(&hw_addr_generated);
         type_out = HW_ADDR_TYPE_GENERATED;
-    } else if (NM_IN_STRSET(addr, NM_CLONED_MAC_STABLE)) {
+    } else if (nm_streq(addr, NM_CLONED_MAC_STABLE)
+               || (is_wifi && nm_streq(addr, NM_CLONED_MAC_STABLE_SSID))) {
+        gs_free char     *stable_id_free = NULL;
         NMUtilsStableType stable_type;
         const char       *stable_id;
+        GBytes           *ssid = NULL;
 
         if (priv->hw_addr_type == HW_ADDR_TYPE_GENERATED) {
             /* hm, we already use a generate MAC address. Most certainly, that is from the same
@@ -17138,7 +17448,28 @@ _hw_addr_get_cloned(NMDevice     *self,
             goto out_no_action;
         }
 
-        stable_id         = _prop_get_connection_stable_id(self, connection, &stable_type);
+        if (!nm_streq(addr, NM_CLONED_MAC_STABLE)) {
+            NMSettingWireless *s_wifi;
+
+            s_wifi = nm_connection_get_setting_wireless(connection);
+            if (s_wifi)
+                ssid = nm_setting_wireless_get_ssid(s_wifi);
+        }
+
+        if (G_UNLIKELY(ssid)) {
+            stable_type = nm_utils_stable_id_parse_network_ssid(ssid,
+                                                                nm_connection_get_uuid(connection),
+                                                                TRUE,
+                                                                &stable_id_free);
+            stable_id   = stable_id_free;
+        } else {
+            /* If @addr is NM_CLONED_MAC_STABLE_SSID, and this is not a Wi-Fi
+             * profile, the behavior is the same as NM_CLONED_MAC_STABLE.  Note
+             * that this really shouldn't happen, because we have a Wi-Fi
+             * profile at hand, and an SSID should be set. */
+            stable_id = _prop_get_connection_stable_id(self, connection, &stable_type);
+        }
+
         hw_addr_generated = nm_utils_hw_addr_gen_stable_eth(
             stable_type,
             stable_id,
@@ -17982,7 +18313,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         nm_assert(priv->type == NM_DEVICE_TYPE_UNKNOWN);
         priv->type = g_value_get_uint(value);
         nm_assert(priv->type > NM_DEVICE_TYPE_UNKNOWN);
-        nm_assert(priv->type <= NM_DEVICE_TYPE_LOOPBACK);
+        nm_assert(priv->type <= NM_DEVICE_TYPE_HSR);
         break;
     case PROP_LINK_TYPE:
         /* construct-only */
@@ -18331,9 +18662,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Capabilities",
                                                            "u",
                                                            NM_DEVICE_CAPABILITIES),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Ip4Address",
-                                                           "u",
-                                                           NM_DEVICE_IP4_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Ip4Address",
+                "u",
+                NM_DEVICE_IP4_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("State", "u", NM_DEVICE_STATE),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("StateReason",
                                                            "(uu)",
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index ff1917b1..3bc03ac1 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -152,7 +152,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_ovs_bridge = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_OVS_BRIDGE,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Slaves",
+                "ao",
+                NM_DEVICE_SLAVES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c
index fd48c2fd..17eb2c2d 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.c
+++ b/src/core/devices/ovs/nm-device-ovs-interface.c
@@ -24,10 +24,21 @@
 
 typedef struct {
     NMOvsdb *ovsdb;
-    GSource *wait_link_idle_source;
-    gulong   wait_link_signal_id;
-    int      wait_link_ifindex;
-    bool     wait_link_is_waiting : 1;
+
+    struct {
+        /* The source for the idle handler to set the TUN ifindex */
+        GSource *tun_set_ifindex_idle_source;
+        /* The cloned MAC to set */
+        char *cloned_mac;
+        /* The id for the signal watching the TUN link to appear/change */
+        gulong tun_link_signal_id;
+        /* The TUN ifindex to set in the idle handler */
+        int tun_ifindex;
+        /* Whether we have determined the cloned MAC */
+        bool cloned_mac_evaluated : 1;
+        /* Whether we are waiting for the kernel link */
+        bool waiting : 1;
+    } wait_link;
 } NMDeviceOvsInterfacePrivate;
 
 struct _NMDeviceOvsInterface {
@@ -46,6 +57,13 @@ G_DEFINE_TYPE(NMDeviceOvsInterface, nm_device_ovs_interface, NM_TYPE_DEVICE)
 
 /*****************************************************************************/
 
+static void _netdev_tun_link_cb(NMPlatform     *platform,
+                                int             obj_type_i,
+                                int             ifindex,
+                                NMPlatformLink *pllink,
+                                int             change_type_i,
+                                NMDevice       *device);
+
 static const char *
 get_type_description(NMDevice *device)
 {
@@ -117,35 +135,65 @@ check_connection_compatible(NMDevice     *device,
     return TRUE;
 }
 
+static gboolean
+check_waiting_for_link(NMDevice *device, const char *from)
+{
+    NMDeviceOvsInterface        *self     = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv     = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+    NMPlatform                  *platform = nm_device_get_platform(device);
+    const NMPlatformLink        *pllink;
+    int                          ip_ifindex;
+    const char                  *reason = NULL;
+
+    if (!priv->wait_link.waiting)
+        return FALSE;
+
+    nm_assert(priv->wait_link.cloned_mac_evaluated);
+    ip_ifindex = nm_device_get_ip_ifindex(device);
+
+    if (ip_ifindex <= 0) {
+        reason = "no ifindex";
+    } else if (!(pllink = nm_platform_link_get(platform, ip_ifindex))) {
+        reason = "platform link not found";
+    } else if (priv->wait_link.cloned_mac
+               && !nm_utils_hwaddr_matches(priv->wait_link.cloned_mac,
+                                           -1,
+                                           pllink->l_address.data,
+                                           pllink->l_address.len)) {
+        reason = "cloned MAC address is not set yet";
+    } else {
+        priv->wait_link.waiting = FALSE;
+    }
+
+    if (priv->wait_link.waiting)
+        _LOGT(LOGD_DEVICE, "ovs-wait-link(%s): not ready: %s", from, reason);
+
+    return priv->wait_link.waiting;
+}
+
 static void
 link_changed(NMDevice *device, const NMPlatformLink *pllink)
 {
-    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device);
+    NMDeviceOvsInterface        *self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
 
-    if (!pllink || !priv->wait_link_is_waiting)
+    if (!pllink || !priv->wait_link.waiting)
         return;
 
-    priv->wait_link_is_waiting = FALSE;
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_IP_CONFIG)
+        return;
 
-    if (nm_device_get_state(device) == NM_DEVICE_STATE_IP_CONFIG) {
-        if (!nm_device_hw_addr_set_cloned(device,
-                                          nm_device_get_applied_connection(device),
-                                          FALSE)) {
-            nm_device_devip_set_failed(device, AF_INET, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
-            nm_device_devip_set_failed(device, AF_INET6, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
-            return;
-        }
+    if (check_waiting_for_link(device, "link-changed"))
+        return;
 
-        nm_device_link_properties_set(device, FALSE);
-        nm_device_bring_up(device);
+    _LOGT(LOGD_CORE, "ovs-wait-link: link is ready after link changed event");
 
-        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);
-        nm_device_activate_schedule_stage3_ip_config(device, FALSE);
-        return;
-    }
+    nm_device_link_properties_set(device, FALSE);
+    nm_device_bring_up(device);
 
-    nm_device_activate_schedule_stage2_device_config(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);
+    nm_device_activate_schedule_stage3_ip_config(device, FALSE);
 }
 
 static gboolean
@@ -205,7 +253,9 @@ set_platform_mtu(NMDevice *device, guint32 mtu)
 static gboolean
 ready_for_ip_config(NMDevice *device, gboolean is_manual)
 {
-    return nm_device_get_ip_ifindex(device) > 0;
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(device);
+
+    return nm_device_get_ip_ifindex(device) > 0 && !priv->wait_link.waiting;
 }
 
 static gboolean
@@ -215,10 +265,28 @@ _set_ip_ifindex_tun(gpointer user_data)
     NMDeviceOvsInterface        *self   = NM_DEVICE_OVS_INTERFACE(device);
     NMDeviceOvsInterfacePrivate *priv   = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
 
-    nm_clear_g_source_inst(&priv->wait_link_idle_source);
+    _LOGT(LOGD_CORE,
+          "ovs-wait-link: setting ip-ifindex %d from tun interface",
+          priv->wait_link.tun_ifindex);
+
+    nm_clear_g_source_inst(&priv->wait_link.tun_set_ifindex_idle_source);
+
+    nm_device_set_ip_ifindex(device, priv->wait_link.tun_ifindex);
+
+    if (check_waiting_for_link(device, "set-ip-ifindex-tun")) {
+        /* If the link is not ready, it means the MAC is not set yet. We don't have
+         * a convenient way to monitor for ip-ifindex changes other than listening
+         * for platform events again.*/
+        nm_assert(!priv->wait_link.tun_link_signal_id);
+        priv->wait_link.tun_link_signal_id = g_signal_connect(nm_device_get_platform(device),
+                                                              NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                                              G_CALLBACK(_netdev_tun_link_cb),
+                                                              self);
+        return G_SOURCE_CONTINUE;
+    }
+
+    _LOGT(LOGD_CORE, "tun link is ready");
 
-    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);
@@ -239,75 +307,149 @@ _netdev_tun_link_cb(NMPlatform     *platform,
     const NMPlatformSignalChangeType change_type = change_type_i;
     NMDeviceOvsInterface            *self        = NM_DEVICE_OVS_INTERFACE(device);
     NMDeviceOvsInterfacePrivate     *priv        = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+    int                              ip_ifindex;
 
-    if (change_type == NM_PLATFORM_SIGNAL_ADDED) {
-        if (pllink->type == NM_LINK_TYPE_TUN
-            && nm_streq0(pllink->name, nm_device_get_iface(device))) {
-            nm_clear_g_signal_handler(platform, &priv->wait_link_signal_id);
+    if (pllink->type != NM_LINK_TYPE_TUN || !nm_streq0(pllink->name, nm_device_get_iface(device)))
+        return;
 
-            priv->wait_link_ifindex = ifindex;
+    ip_ifindex = nm_device_get_ip_ifindex(device);
+    if (ip_ifindex > 0) {
+        /* When we have an ifindex, we are only waiting for the MAC to settle */
+        if (change_type != NM_PLATFORM_SIGNAL_CHANGED)
+            return;
 
-            priv->wait_link_idle_source = nm_g_idle_add_source(_set_ip_ifindex_tun, device);
+        if (!check_waiting_for_link(device, "tun-link-changed")) {
+            _LOGT(LOGD_CORE, "ovs-wait-link: tun link is ready, cloned MAC is set");
+
+            nm_clear_g_signal_handler(platform, &priv->wait_link.tun_link_signal_id);
+            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);
+            nm_device_activate_schedule_stage3_ip_config(device, FALSE);
         }
+        return;
     }
+
+    /* No ip-ifindex on the device, set it when the link appears */
+    if (change_type != NM_PLATFORM_SIGNAL_ADDED)
+        return;
+
+    _LOGT(LOGD_CORE,
+          "ovs-wait-link: found matching tun interface, schedule set-ip-ifindex(%d)",
+          ifindex);
+    nm_clear_g_signal_handler(platform, &priv->wait_link.tun_link_signal_id);
+    priv->wait_link.tun_ifindex                 = ifindex;
+    priv->wait_link.tun_set_ifindex_idle_source = nm_g_idle_add_source(_set_ip_ifindex_tun, device);
+}
+
+static gboolean
+ovs_interface_is_netdev_datapath(NMDeviceOvsInterface *self)
+{
+    NMDevice           *device       = NM_DEVICE(self);
+    NMActiveConnection *ac           = NULL;
+    NMSettingOvsBridge *s_ovs_bridge = NULL;
+
+    ac = NM_ACTIVE_CONNECTION(nm_device_get_act_request(device));
+    if (!ac)
+        return FALSE;
+
+    /* get ovs-port active-connection */
+    ac = nm_active_connection_get_master(ac);
+    if (!ac)
+        return FALSE;
+
+    /* get ovs-bridge active-connection */
+    ac = nm_active_connection_get_master(ac);
+    if (!ac)
+        return FALSE;
+
+    s_ovs_bridge =
+        nm_connection_get_setting_ovs_bridge(nm_active_connection_get_applied_connection(ac));
+    if (!s_ovs_bridge)
+        return FALSE;
+
+    return nm_streq0(nm_setting_ovs_bridge_get_datapath_type(s_ovs_bridge), "netdev");
 }
 
 static void
 act_stage3_ip_config(NMDevice *device, int addr_family)
 {
-    NMActiveConnection          *controller_act = NULL;
-    NMSettingOvsBridge          *s_ovs_bridge   = NULL;
-    NMDeviceOvsInterface        *self           = NM_DEVICE_OVS_INTERFACE(device);
-    NMDeviceOvsInterfacePrivate *priv           = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+    NMDeviceOvsInterface        *self = NM_DEVICE_OVS_INTERFACE(device);
+    NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
+
+    /*
+     * When the ovs-interface device enters stage3, it becomes eligible to be attached to
+     * its controller (a ovs-port). If also the ovs-bridge is ready, an entry is created
+     * in the ovsdb in NMDeviceOvsPort->attach_port().
+     * FIXME(l3cfg): we should create the IP ifindex before stage3 start.
+     *
+     * NMDeviceOvsInterface->act_stage3_ip_config() is supposed to perform device-specific
+     * IP configuration on the device. An ovs-interface can be of different types, that
+     * require different handling:
+     *
+     *  - "patch" and "dpdk" interfaces don't have any kernel link associated and thus
+     *  NetworkManager completely skips any kind of IP configuration on them, by returning
+     *  FALSE to ->ready_for_ip_config().
+     *
+     *  - "system" interfaces represent other interface types with kernel link (for
+     *  example, ethernet, bond, etc.) that get attached to a ovs bridge. Once they are
+     *  attached, NetworkManager can start the IP configuration right away.
+     *
+     *  - "internal" interfaces are virtual interfaces created by openvswitch. Once the
+     *  entry is created in the ovsdb, the kernel will create a link for the
+     *  interface. When using the system datapath (the default), the link is of type
+     *  "openvswitch", while when using the netdev (userspace) datapath, the link is a tun
+     *  (tap) one. For both datapath types, we use this method to delay the IP
+     *  configuration until the link appears. Note that ready_for_ip_config() returns FALSE
+     *  when there is no ifindex, and so all the regular IP methods (static, auto, etc.)
+     *  can't proceed.
+     */
 
     if (!_is_internal_interface(device)) {
         nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
         return;
     }
 
-    /* When the ovs-bridge controller is using netdev datapath, the interface
-     * link created is a tun device instead of a ovs-interface. NetworkManager must
-     * detect the creation of the tun link and attach the ifindex to the
-     * ovs-interface device. */
-    controller_act = NM_ACTIVE_CONNECTION(nm_device_get_act_request(device));
-    if (controller_act && nm_device_get_ip_ifindex(device) <= 0 && priv->wait_link_signal_id == 0) {
-        controller_act = nm_active_connection_get_master(controller_act);
-        if (controller_act) {
-            controller_act = nm_active_connection_get_master(controller_act);
-            if (controller_act)
-                s_ovs_bridge = nm_connection_get_setting_ovs_bridge(
-                    nm_active_connection_get_applied_connection(controller_act));
-            if (s_ovs_bridge
-                && nm_streq0(nm_setting_ovs_bridge_get_datapath_type(s_ovs_bridge), "netdev"))
-                priv->wait_link_signal_id = g_signal_connect(nm_device_get_platform(device),
-                                                             NM_PLATFORM_SIGNAL_LINK_CHANGED,
-                                                             G_CALLBACK(_netdev_tun_link_cb),
-                                                             self);
-        }
+    /*
+    * If a ovs interface has the cloned-mac-address property set, we pass the desired MAC
+    * to ovsdb when creating the db entry, and openvswitch will eventually assign it to
+    * the interface. Note that usually the link will not have the desired MAC when it's
+    * created, and so we need to also monitor link changes to detect when the MAC is
+    * ready; only after that we can start IP configuration. Otherwise, the ARP
+    * announcements, the DHCP client-id, etc will use the wrong MAC.
+    */
+    if (!priv->wait_link.cloned_mac_evaluated) {
+        nm_assert(!priv->wait_link.cloned_mac);
+        nm_device_hw_addr_get_cloned(device,
+                                     nm_device_get_applied_connection(device),
+                                     FALSE,
+                                     &priv->wait_link.cloned_mac,
+                                     NULL,
+                                     NULL);
+        priv->wait_link.cloned_mac_evaluated = TRUE;
     }
 
-    /* FIXME(l3cfg): we should create the IP ifindex before stage3 start.
-     *
-     * For now it's here because when the ovs-interface enters stage3, then it's added to the
-     * controller (ovs-port) and the entry is create in the ovsdb. Only after that the kernel
-     * link appears.
-     *
-     * This should change. */
-    if (nm_device_get_ip_ifindex(device) <= 0) {
-        _LOGT(LOGD_DEVICE, "waiting for link to appear");
-        priv->wait_link_is_waiting = TRUE;
+    priv->wait_link.waiting = TRUE;
+    if (check_waiting_for_link(device, addr_family == AF_INET ? "stage3-ipv4" : "stage3-ipv6")) {
         nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_PENDING, NULL);
+        if (nm_device_get_ip_ifindex(device) <= 0 && priv->wait_link.tun_link_signal_id == 0
+            && ovs_interface_is_netdev_datapath(self)) {
+            priv->wait_link.tun_link_signal_id = g_signal_connect(nm_device_get_platform(device),
+                                                                  NM_PLATFORM_SIGNAL_LINK_CHANGED,
+                                                                  G_CALLBACK(_netdev_tun_link_cb),
+                                                                  self);
+        }
         return;
     }
 
-    priv->wait_link_is_waiting = FALSE;
-    nm_clear_g_source_inst(&priv->wait_link_idle_source);
-    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->wait_link_signal_id);
+    _LOGT(LOGD_DEVICE,
+          "ovs-wait-link: link is ready, IPv%c can proceed",
+          nm_utils_addr_family_to_char(addr_family));
 
-    if (!nm_device_hw_addr_set_cloned(device, nm_device_get_applied_connection(device), FALSE)) {
-        nm_device_devip_set_failed(device, addr_family, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
-        return;
-    }
+    priv->wait_link.waiting = FALSE;
+    nm_clear_g_source_inst(&priv->wait_link.tun_set_ifindex_idle_source);
+    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->wait_link.tun_link_signal_id);
 
     nm_device_link_properties_set(device, FALSE);
     nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, NULL);
@@ -325,8 +467,11 @@ deactivate(NMDevice *device)
     NMDeviceOvsInterface        *self = NM_DEVICE_OVS_INTERFACE(device);
     NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
 
-    priv->wait_link_is_waiting = FALSE;
-    nm_clear_g_source_inst(&priv->wait_link_idle_source);
+    priv->wait_link.waiting              = FALSE;
+    priv->wait_link.cloned_mac_evaluated = FALSE;
+    nm_clear_g_free(&priv->wait_link.cloned_mac);
+    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->wait_link.tun_link_signal_id);
+    nm_clear_g_source_inst(&priv->wait_link.tun_set_ifindex_idle_source);
 }
 
 typedef struct {
@@ -418,13 +563,27 @@ deactivate_async(NMDevice                  *device,
 
     _LOGT(LOGD_CORE, "deactivate: start async");
 
-    /* We want to ensure that the kernel link for this device is
-     * removed upon disconnection so that it will not interfere with
-     * later activations of the same device. Unfortunately there is
-     * no synchronization mechanism with vswitchd, we only update
-     * ovsdb and wait that changes are picked up.
-     */
+    nm_clear_g_signal_handler(nm_device_get_platform(device), &priv->wait_link.tun_link_signal_id);
+    nm_clear_g_source_inst(&priv->wait_link.tun_set_ifindex_idle_source);
+    priv->wait_link.cloned_mac_evaluated = FALSE;
+    nm_clear_g_free(&priv->wait_link.cloned_mac);
 
+    /* We want to ensure that the kernel link for this device is removed upon
+     * disconnection, so that it will not interfere with later activations of the same
+     * device.
+     *
+     * To do so, we need to be very careful, because unfortunately there is no
+     * synchronization mechanism with vswitchd: we only update ovsdb, wait that changes
+     * are picked up and we see the effects on the kernel interface (appearing or going
+     * away).
+     *
+     * That means for example that if the ovs interface entered stage3 and the entry was
+     * added to the ovsdb, we expect a link to appear. If we disconnect at this point, we
+     * delete the entry from the ovsdb. Now we don't know if ovs-vswitchd will see two
+     * updates or only one. In other words, we don't know if the interface will appear and
+     * go away, or if it will not appear ever. In this situation, the solution is to wait
+     * with a timeout.
+     */
     data  = g_slice_new(DeactivateData);
     *data = (DeactivateData){
         .self               = g_object_ref(self),
@@ -433,7 +592,7 @@ deactivate_async(NMDevice                  *device,
         .callback_user_data = callback_user_data,
     };
 
-    if (!priv->wait_link_is_waiting
+    if (!priv->wait_link.waiting
         && !nm_platform_link_get_by_ifname(nm_device_get_platform(device),
                                            nm_device_get_iface(device))) {
         _LOGT(LOGD_CORE, "deactivate: link not present, proceeding");
@@ -442,20 +601,15 @@ deactivate_async(NMDevice                  *device,
         return;
     }
 
-    nm_clear_g_source_inst(&priv->wait_link_idle_source);
-
-    if (priv->wait_link_is_waiting) {
-        /* At this point we have issued an INSERT and a DELETE
-         * command for the interface to ovsdb. We don't know if
-         * vswitchd will see the two updates or only one. We
-         * must add a timeout to avoid waiting forever in case
-         * the link doesn't appear.
-         */
+    if (priv->wait_link.waiting) {
+        /* Here we have issued an INSERT and a DELETE command for the interface to ovsdb,
+         * and must wait with a timeout. */
         data->link_timeout_id = g_timeout_add(6000, deactivate_link_timeout, data);
         _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear in 6 seconds");
     } else
         _LOGT(LOGD_DEVICE, "deactivate: waiting for link to disappear");
 
+    priv->wait_link.waiting = FALSE;
     data->cancelled_id =
         g_cancellable_connect(cancellable, G_CALLBACK(deactivate_cancelled_cb), data, NULL);
     data->link_changed_id = g_signal_connect(nm_device_get_platform(device),
@@ -506,6 +660,10 @@ dispose(GObject *object)
     NMDeviceOvsInterface        *self = NM_DEVICE_OVS_INTERFACE(object);
     NMDeviceOvsInterfacePrivate *priv = NM_DEVICE_OVS_INTERFACE_GET_PRIVATE(self);
 
+    nm_assert(!priv->wait_link.waiting);
+    nm_assert(priv->wait_link.tun_link_signal_id == 0);
+    nm_assert(!priv->wait_link.tun_set_ifindex_idle_source);
+
     if (priv->ovsdb) {
         g_signal_handlers_disconnect_by_func(priv->ovsdb, G_CALLBACK(ovsdb_ready), self);
         g_clear_object(&priv->ovsdb);
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
index 5ede46e9..258c72f2 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.c
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -266,7 +266,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_ovs_port = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_OVS_PORT,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Slaves",
+                "ao",
+                NM_DEVICE_SLAVES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/devices/team/nm-device-team.c b/src/core/devices/team/nm-device-team.c
index 4d748362..d8c3c2c2 100644
--- a/src/core/devices/team/nm-device-team.c
+++ b/src/core/devices/team/nm-device-team.c
@@ -290,9 +290,9 @@ master_update_slave_connection(NMDevice     *device,
     g_free(port_config);
 
     g_object_set(nm_connection_get_setting_connection(connection),
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  nm_connection_get_uuid(applied_connection),
-                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_CONNECTION_PORT_TYPE,
                  NM_SETTING_TEAM_SETTING_NAME,
                  NULL);
     return TRUE;
@@ -1090,9 +1090,21 @@ static const NMDBusInterfaceInfoExtended interface_info_device_team = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_TEAM,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Carrier", "b", NM_DEVICE_CARRIER),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Slaves", "ao", NM_DEVICE_SLAVES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Carrier",
+                "b",
+                NM_DEVICE_CARRIER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Slaves",
+                "ao",
+                NM_DEVICE_SLAVES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Config",
                                                            "s",
                                                            NM_DEVICE_TEAM_CONFIG), ), ),
diff --git a/src/core/devices/wifi/nm-device-iwd-p2p.c b/src/core/devices/wifi/nm-device-iwd-p2p.c
index 73fd4716..fadc6722 100644
--- a/src/core/devices/wifi/nm-device-iwd-p2p.c
+++ b/src/core/devices/wifi/nm-device-iwd-p2p.c
@@ -1144,7 +1144,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p = {
         .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_wifi_p2p_peer_added,
                                                 &nm_signal_info_wifi_p2p_peer_removed, ),
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Peers",
                                                            "ao",
                                                            NM_DEVICE_IWD_P2P_PEERS), ), ),
diff --git a/src/core/devices/wifi/nm-device-olpc-mesh.c b/src/core/devices/wifi/nm-device-olpc-mesh.c
index 436c7847..8e1e779b 100644
--- a/src/core/devices/wifi/nm-device-olpc-mesh.c
+++ b/src/core/devices/wifi/nm-device-olpc-mesh.c
@@ -497,7 +497,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_olpc_mesh = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_DEVICE_OLPC_MESH,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Companion",
                                                            "o",
                                                            NM_DEVICE_OLPC_MESH_COMPANION),
diff --git a/src/core/devices/wifi/nm-device-wifi-p2p.c b/src/core/devices/wifi/nm-device-wifi-p2p.c
index fa8cb8fa..981811a3 100644
--- a/src/core/devices/wifi/nm-device-wifi-p2p.c
+++ b/src/core/devices/wifi/nm-device-wifi-p2p.c
@@ -15,6 +15,7 @@
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device-private.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
+#include "libnm-core-aux-intern/nm-common-macros.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-glib-aux/nm-ref-string.h"
 #include "libnm-platform/nm-platform.h"
@@ -982,23 +983,24 @@ device_state_changed(NMDevice           *device,
 }
 
 static void
-impl_device_wifi_p2p_start_find(NMDBusObject                      *obj,
-                                const NMDBusInterfaceInfoExtended *interface_info,
-                                const NMDBusMethodInfoExtended    *method_info,
-                                GDBusConnection                   *connection,
-                                const char                        *sender,
-                                GDBusMethodInvocation             *invocation,
-                                GVariant                          *parameters)
+p2p_start_find_auth_cb(NMDevice              *device,
+                       GDBusMethodInvocation *invocation,
+                       NMAuthSubject         *subject,
+                       GError                *error,
+                       gpointer               user_data)
 {
-    NMDeviceWifiP2P           *self    = NM_DEVICE_WIFI_P2P(obj);
+    NMDeviceWifiP2P           *self    = NM_DEVICE_WIFI_P2P(device);
     NMDeviceWifiP2PPrivate    *priv    = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
-    gs_unref_variant GVariant *options = NULL;
+    gs_unref_variant GVariant *options = user_data;
     const char                *opts_key;
     GVariant                  *opts_val;
     GVariantIter               iter;
     gint32                     timeout = 30;
 
-    g_variant_get(parameters, "(@a{sv})", &options);
+    if (error) {
+        g_dbus_method_invocation_return_gerror(invocation, error);
+        return;
+    }
 
     g_variant_iter_init(&iter, options);
     while (g_variant_iter_next(&iter, "{&sv}", &opts_key, &opts_val)) {
@@ -1050,17 +1052,43 @@ impl_device_wifi_p2p_start_find(NMDBusObject                      *obj,
 }
 
 static void
-impl_device_wifi_p2p_stop_find(NMDBusObject                      *obj,
-                               const NMDBusInterfaceInfoExtended *interface_info,
-                               const NMDBusMethodInfoExtended    *method_info,
-                               GDBusConnection                   *connection,
-                               const char                        *sender,
-                               GDBusMethodInvocation             *invocation,
-                               GVariant                          *parameters)
+impl_device_wifi_p2p_start_find(NMDBusObject                      *obj,
+                                const NMDBusInterfaceInfoExtended *interface_info,
+                                const NMDBusMethodInfoExtended    *method_info,
+                                GDBusConnection                   *connection,
+                                const char                        *sender,
+                                GDBusMethodInvocation             *invocation,
+                                GVariant                          *parameters)
+{
+    gs_unref_variant GVariant *options = NULL;
+
+    g_variant_get(parameters, "(@a{sv})", &options);
+
+    nm_device_auth_request(NM_DEVICE(obj),
+                           invocation,
+                           NULL,
+                           NM_AUTH_PERMISSION_WIFI_SCAN,
+                           TRUE,
+                           NULL,
+                           p2p_start_find_auth_cb,
+                           g_steal_pointer(&options));
+}
+
+static void
+p2p_stop_find_auth_cb(NMDevice              *device,
+                      GDBusMethodInvocation *invocation,
+                      NMAuthSubject         *subject,
+                      GError                *error,
+                      gpointer               user_data)
 {
-    NMDeviceWifiP2P        *self = NM_DEVICE_WIFI_P2P(obj);
+    NMDeviceWifiP2P        *self = NM_DEVICE_WIFI_P2P(device);
     NMDeviceWifiP2PPrivate *priv = NM_DEVICE_WIFI_P2P_GET_PRIVATE(self);
 
+    if (error) {
+        g_dbus_method_invocation_return_gerror(invocation, error);
+        return;
+    }
+
     if (!priv->mgmt_iface) {
         g_dbus_method_invocation_return_error_literal(
             invocation,
@@ -1075,6 +1103,25 @@ impl_device_wifi_p2p_stop_find(NMDBusObject                      *obj,
     g_dbus_method_invocation_return_value(invocation, NULL);
 }
 
+static void
+impl_device_wifi_p2p_stop_find(NMDBusObject                      *obj,
+                               const NMDBusInterfaceInfoExtended *interface_info,
+                               const NMDBusMethodInfoExtended    *method_info,
+                               GDBusConnection                   *connection,
+                               const char                        *sender,
+                               GDBusMethodInvocation             *invocation,
+                               GVariant                          *parameters)
+{
+    nm_device_auth_request(NM_DEVICE(obj),
+                           invocation,
+                           NULL,
+                           NM_AUTH_PERMISSION_WIFI_SCAN,
+                           TRUE,
+                           NULL,
+                           p2p_stop_find_auth_cb,
+                           NULL);
+}
+
 /*****************************************************************************/
 
 NMSupplicantInterface *
@@ -1171,7 +1218,11 @@ static const NMDBusInterfaceInfoExtended interface_info_device_wifi_p2p = {
         .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_wifi_p2p_peer_added,
                                                 &nm_signal_info_wifi_p2p_peer_removed, ),
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "HwAddress",
+                "s",
+                NM_DEVICE_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Peers",
                                                            "ao",
                                                            NM_DEVICE_WIFI_P2P_PEERS), ), ),
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 43772834..2cd41b27 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -3191,27 +3191,28 @@ ensure_hotspot_frequency(NMDeviceWifi *self, NMSettingWireless *s_wifi, NMWifiAP
         GBytes       *ssid;
         gsize         ssid_len;
         const guint8 *ssid_data;
-        const guint8  random_seed[16] = {0x9a,
-                                         0xdc,
-                                         0x86,
-                                         0x9a,
-                                         0xa8,
-                                         0xa2,
-                                         0x07,
-                                         0x97,
-                                         0xbe,
-                                         0x6d,
-                                         0xe6,
-                                         0x99,
-                                         0x9f,
-                                         0xa8,
-                                         0x09,
-                                         0x2b};
 
         /* Calculate a stable "random" number based on the SSID. */
         ssid      = nm_setting_wireless_get_ssid(s_wifi);
         ssid_data = g_bytes_get_data(ssid, &ssid_len);
-        rnd       = c_siphash_hash(random_seed, ssid_data, ssid_len);
+        rnd       = c_siphash_hash(NM_HASH_SEED_16(0x9a,
+                                             0xdc,
+                                             0x86,
+                                             0x9a,
+                                             0xa8,
+                                             0xa2,
+                                             0x07,
+                                             0x97,
+                                             0xbe,
+                                             0x6d,
+                                             0xe6,
+                                             0x99,
+                                             0x9f,
+                                             0xa8,
+                                             0x09,
+                                             0x2b),
+                             ssid_data,
+                             ssid_len);
     }
 
     if (nm_streq0(band, "a")) {
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 0d4daf21..76a342e2 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -810,7 +810,7 @@ sett_conn_changed(NMSettingsConnection   *sett_conn,
             nm_log_dbg(LOGD_WIFI,
                        "iwd: profile at %s not removed: %s (%i)",
                        orig_full_path,
-                       strerror(errno),
+                       nm_strerror_native(errno),
                        errno);
 
         removed = TRUE;
@@ -1431,7 +1431,10 @@ try_delete_file:
     if (g_remove(full_path) == 0)
         _LOGD("IWD profile at %s removed", full_path);
     else if (errno != ENOENT)
-        _LOGD("IWD profile at %s not removed: %s (%i)", full_path, strerror(errno), errno);
+        _LOGD("IWD profile at %s not removed: %s (%i)",
+              full_path,
+              nm_strerror_native(errno),
+              errno);
 }
 
 static void
@@ -1899,7 +1902,7 @@ nm_iwd_manager_get_ap_mirror_connection(NMIwdManager *self, NMWifiAP *ap)
     NM80211ApSecurityFlags sec_flags = nm_wifi_ap_get_wpa_flags(ap) | nm_wifi_ap_get_rsn_flags(ap);
 
     ssid_bytes = g_bytes_get_data(nm_wifi_ap_get_ssid(ap), &ssid_len);
-    ssid_len   = MIN(ssid_len, 32);
+    ssid_len   = NM_MIN(ssid_len, 32u);
     memcpy(name_buf, ssid_bytes, ssid_len);
     name_buf[ssid_len] = '\0';
 
diff --git a/src/core/devices/wifi/nm-wifi-ap.c b/src/core/devices/wifi/nm-wifi-ap.c
index 825a5aa8..d4d3815e 100644
--- a/src/core/devices/wifi/nm-wifi-ap.c
+++ b/src/core/devices/wifi/nm-wifi-ap.c
@@ -36,6 +36,7 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMWifiAP,
                              PROP_HW_ADDRESS,
                              PROP_MODE,
                              PROP_MAX_BITRATE,
+                             PROP_BANDWIDTH,
                              PROP_STRENGTH,
                              PROP_LAST_SEEN, );
 
@@ -47,6 +48,7 @@ struct _NMWifiAPPrivate {
     guint8       strength;
     guint32      freq;        /* Frequency in MHz; ie 2412 (== 2.412 GHz) */
     guint32      max_bitrate; /* Maximum bitrate of the AP in Kbit/s (ie 54000 Kb/s == 54Mbit/s) */
+    guint32      bandwidth;   /* Bandwidth of the AP in MHz */
 
     gint64
         last_seen_msec; /* Timestamp when the AP was seen lastly (in nm_utils_get_monotonic_timestamp_*() scale).
@@ -277,6 +279,32 @@ nm_wifi_ap_get_max_bitrate(NMWifiAP *ap)
     return NM_WIFI_AP_GET_PRIVATE(ap)->max_bitrate;
 }
 
+guint32
+nm_wifi_ap_get_bandwidth(NMWifiAP *ap)
+{
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), 0);
+    g_return_val_if_fail(nm_dbus_object_is_exported(NM_DBUS_OBJECT(ap)), 0);
+
+    return NM_WIFI_AP_GET_PRIVATE(ap)->bandwidth;
+}
+
+gboolean
+nm_wifi_ap_set_bandwidth(NMWifiAP *ap, guint32 bandwidth)
+{
+    NMWifiAPPrivate *priv;
+
+    g_return_val_if_fail(NM_IS_WIFI_AP(ap), FALSE);
+
+    priv = NM_WIFI_AP_GET_PRIVATE(ap);
+
+    if (priv->bandwidth != bandwidth) {
+        priv->bandwidth = bandwidth;
+        _notify(ap, PROP_BANDWIDTH);
+        return TRUE;
+    }
+    return FALSE;
+}
+
 gboolean
 nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate)
 {
@@ -393,6 +421,7 @@ nm_wifi_ap_update_from_properties(NMWifiAP *ap, const NMSupplicantBssInfo *bss_i
     }
 
     changed |= nm_wifi_ap_set_max_bitrate(ap, bss_info->max_rate);
+    changed |= nm_wifi_ap_set_bandwidth(ap, bss_info->bandwidth);
 
     if (priv->metered != bss_info->metered) {
         priv->metered = bss_info->metered;
@@ -683,6 +712,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_MAX_BITRATE:
         g_value_set_uint(value, priv->max_bitrate);
         break;
+    case PROP_BANDWIDTH:
+        g_value_set_uint(value, priv->bandwidth);
+        break;
     case PROP_STRENGTH:
         g_value_set_uchar(value, priv->strength);
         break;
@@ -873,6 +905,7 @@ static const NMDBusInterfaceInfoExtended interface_info_access_point = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("MaxBitrate",
                                                            "u",
                                                            NM_WIFI_AP_MAX_BITRATE),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Bandwidth", "u", NM_WIFI_AP_BANDWIDTH),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Strength", "y", NM_WIFI_AP_STRENGTH),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("LastSeen",
                                                            "i",
@@ -979,6 +1012,14 @@ nm_wifi_ap_class_init(NMWifiAPClass *ap_class)
                                                       -1,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_BANDWIDTH] = g_param_spec_uint(NM_WIFI_AP_BANDWIDTH,
+                                                       "",
+                                                       "",
+                                                       0,
+                                                       G_MAXUINT32,
+                                                       0,
+                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 }
 
diff --git a/src/core/devices/wifi/nm-wifi-ap.h b/src/core/devices/wifi/nm-wifi-ap.h
index 93dc8edf..0f229640 100644
--- a/src/core/devices/wifi/nm-wifi-ap.h
+++ b/src/core/devices/wifi/nm-wifi-ap.h
@@ -27,6 +27,7 @@
 #define NM_WIFI_AP_HW_ADDRESS  "hw-address"
 #define NM_WIFI_AP_MODE        "mode"
 #define NM_WIFI_AP_MAX_BITRATE "max-bitrate"
+#define NM_WIFI_AP_BANDWIDTH   "bandwidth"
 #define NM_WIFI_AP_STRENGTH    "strength"
 #define NM_WIFI_AP_LAST_SEEN   "last-seen"
 
@@ -78,6 +79,8 @@ guint32                nm_wifi_ap_get_freq(NMWifiAP *ap);
 gboolean               nm_wifi_ap_set_freq(NMWifiAP *ap, guint32 freq);
 guint32                nm_wifi_ap_get_max_bitrate(NMWifiAP *ap);
 gboolean               nm_wifi_ap_set_max_bitrate(NMWifiAP *ap, guint32 bitrate);
+guint32                nm_wifi_ap_get_bandwidth(NMWifiAP *ap);
+gboolean               nm_wifi_ap_set_bandwidth(NMWifiAP *ap, guint32 bandwidth);
 gboolean               nm_wifi_ap_get_fake(const NMWifiAP *ap);
 gboolean               nm_wifi_ap_set_fake(NMWifiAP *ap, gboolean fake);
 NM80211ApFlags         nm_wifi_ap_get_flags(const NMWifiAP *self);
diff --git a/src/core/devices/wifi/nm-wifi-common.c b/src/core/devices/wifi/nm-wifi-common.c
index 681d9cb5..e3639ec5 100644
--- a/src/core/devices/wifi/nm-wifi-common.c
+++ b/src/core/devices/wifi/nm-wifi-common.c
@@ -132,7 +132,8 @@ const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless = {
                 NM_DEFINE_GDBUS_METHOD_INFO_INIT(
                     "GetAccessPoints",
                     .out_args = NM_DEFINE_GDBUS_ARG_INFOS(
-                        NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ), ),
+                        NM_DEFINE_GDBUS_ARG_INFO("access_points", "ao"), ),
+                    .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
                 .handle = impl_device_wifi_get_access_points, ),
             NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(
                 NM_DEFINE_GDBUS_METHOD_INFO_INIT(
@@ -150,9 +151,11 @@ const NMDBusInterfaceInfoExtended nm_interface_info_device_wireless = {
                                                 &nm_signal_info_wireless_access_point_removed, ),
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("HwAddress", "s", NM_DEVICE_HW_ADDRESS),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("PermHwAddress",
-                                                           "s",
-                                                           NM_DEVICE_PERM_HW_ADDRESS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "PermHwAddress",
+                "s",
+                NM_DEVICE_PERM_HW_ADDRESS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Mode", "u", NM_DEVICE_WIFI_MODE),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Bitrate", "u", NM_DEVICE_WIFI_BITRATE),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AccessPoints",
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 6978bd3c..4be03f4b 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -527,7 +527,7 @@ _acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log)
 
     nm_clear_l3cd(&reglist_data->l3cd);
 
-    nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_UPDATE);
+    nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
 
     g_array_remove_index(priv->v4.acd.reglist, idx);
 
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index 903ea6ac..4dd2c4fd 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -162,6 +162,9 @@ typedef struct {
              * will always broadcast replies. */
             bool request_broadcast : 1;
 
+            /* Whether to send or not the client identifier */
+            bool send_client_id : 1;
+
         } v4;
         struct {
             /* If set, the DUID from the connection is used; otherwise
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.c b/src/core/dhcp/nm-dhcp-dhclient-utils.c
index ea8943fa..286f7aa1 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.c
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.c
@@ -120,7 +120,10 @@ add_ip4_config(GString            *str,
         }
 
         g_string_append(str, CLIENTID_TAG " ");
-        if (i < l) {
+        if (l == 0) {
+            /* An empty value effectively unsets the client-id to avoid sending it */
+            g_string_append(str, "\"\"");
+        } else if (i < l) {
             /* Unprintable; convert to a hex string */
             for (i = 0; i < l; i++) {
                 if (i > 0)
@@ -293,6 +296,7 @@ char *
 nm_dhcp_dhclient_create_config(const char         *interface,
                                int                 addr_family,
                                GBytes             *client_id,
+                               gboolean            send_client_id,
                                const char         *anycast_address,
                                const char         *hostname,
                                guint32             timeout,
@@ -391,8 +395,8 @@ nm_dhcp_dhclient_create_config(const char         *interface,
                 continue;
 
             if (NM_STR_HAS_PREFIX(p, CLIENTID_TAG)) {
-                /* Override config file "dhcp-client-id" and use one from the connection */
-                if (client_id)
+                /* Skip "dhcp-client-id" if the connection has defined a custom one or "none" */
+                if (client_id || !send_client_id)
                     continue;
 
                 /* Otherwise, capture and return the existing client id */
@@ -477,6 +481,8 @@ nm_dhcp_dhclient_create_config(const char         *interface,
     }
 
     if (addr_family == AF_INET) {
+        nm_auto_unref_bytes GBytes *client_id_none = NULL;
+        client_id = send_client_id ? client_id : (client_id_none = g_bytes_new_static("", 0));
         add_ip4_config(new_contents, client_id, hostname, use_fqdn, hostname_flags);
         add_request(reqs, "rfc3442-classless-static-routes");
         add_request(reqs, "ms-classless-static-routes");
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.h b/src/core/dhcp/nm-dhcp-dhclient-utils.h
index 6187cce0..34b26175 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.h
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.h
@@ -12,6 +12,7 @@
 char *nm_dhcp_dhclient_create_config(const char         *interface,
                                      int                 addr_family,
                                      GBytes             *client_id,
+                                     gboolean            send_client_id,
                                      const char         *anycast_addr,
                                      const char         *hostname,
                                      guint32             timeout,
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
index 4aab4b1e..2a3af10a 100644
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ b/src/core/dhcp/nm-dhcp-dhclient.c
@@ -148,56 +148,6 @@ get_dhclient_leasefile(int         addr_family,
     return NULL;
 }
 
-static gboolean
-merge_dhclient_config(NMDhcpDhclient     *self,
-                      int                 addr_family,
-                      const char         *iface,
-                      const char         *conf_file,
-                      GBytes             *client_id,
-                      const char         *anycast_address,
-                      const char         *hostname,
-                      guint32             timeout,
-                      gboolean            use_fqdn,
-                      NMDhcpHostnameFlags hostname_flags,
-                      const char         *mud_url,
-                      const char *const  *reject_servers,
-                      const char         *orig_path,
-                      GBytes            **out_new_client_id,
-                      GError            **error)
-{
-    gs_free char *orig = NULL;
-    gs_free char *new  = NULL;
-
-    g_return_val_if_fail(iface, FALSE);
-    g_return_val_if_fail(conf_file, FALSE);
-
-    if (orig_path && g_file_test(orig_path, G_FILE_TEST_EXISTS)) {
-        GError *read_error = NULL;
-
-        if (!g_file_get_contents(orig_path, &orig, NULL, &read_error)) {
-            _LOGW("error reading dhclient configuration %s: %s", orig_path, read_error->message);
-            g_error_free(read_error);
-        }
-    }
-
-    new = nm_dhcp_dhclient_create_config(iface,
-                                         addr_family,
-                                         client_id,
-                                         anycast_address,
-                                         hostname,
-                                         timeout,
-                                         use_fqdn,
-                                         hostname_flags,
-                                         mud_url,
-                                         reject_servers,
-                                         orig_path,
-                                         orig,
-                                         out_new_client_id);
-    nm_assert(new);
-
-    return g_file_set_contents(conf_file, new, -1, error);
-}
-
 static char *
 find_existing_config(NMDhcpDhclient *self, int addr_family, const char *iface, const char *uuid)
 {
@@ -283,6 +233,7 @@ create_dhclient_config(NMDhcpDhclient     *self,
                        const char         *iface,
                        const char         *uuid,
                        GBytes             *client_id,
+                       gboolean            send_client_id,
                        const char         *anycast_address,
                        const char         *hostname,
                        guint32             timeout,
@@ -292,44 +243,56 @@ create_dhclient_config(NMDhcpDhclient     *self,
                        const char *const  *reject_servers,
                        GBytes            **out_new_client_id)
 {
-    gs_free char *orig = NULL;
-    char *new          = NULL;
-    GError *error      = NULL;
+    gs_free char *orig_path    = NULL;
+    gs_free char *orig_content = NULL;
+    char         *new_path     = NULL;
+    gs_free char *new_content  = NULL;
+    GError       *error        = NULL;
 
     g_return_val_if_fail(iface != NULL, NULL);
 
-    new = g_strdup_printf(NMSTATEDIR "/dhclient%s-%s.conf",
-                          _addr_family_to_path_part(addr_family),
-                          iface);
-
-    _LOGD("creating composite dhclient config %s", new);
+    new_path = g_strdup_printf(NMSTATEDIR "/dhclient%s-%s.conf",
+                               _addr_family_to_path_part(addr_family),
+                               iface);
+    _LOGD("creating composite dhclient config %s", new_path);
 
-    orig = find_existing_config(self, addr_family, iface, uuid);
-    if (orig)
-        _LOGD("merging existing dhclient config %s", orig);
+    orig_path = find_existing_config(self, addr_family, iface, uuid);
+    if (orig_path)
+        _LOGD("merging existing dhclient config %s", orig_path);
     else
         _LOGD("no existing dhclient configuration to merge");
 
-    if (!merge_dhclient_config(self,
-                               addr_family,
-                               iface,
-                               new,
-                               client_id,
-                               anycast_address,
-                               hostname,
-                               timeout,
-                               use_fqdn,
-                               hostname_flags,
-                               mud_url,
-                               reject_servers,
-                               orig,
-                               out_new_client_id,
-                               &error)) {
+    if (orig_path && g_file_test(orig_path, G_FILE_TEST_EXISTS)) {
+        if (!g_file_get_contents(orig_path, &orig_content, NULL, &error)) {
+            _LOGW("error reading dhclient configuration %s: %s", orig_path, error->message);
+            g_error_free(error);
+        }
+    }
+
+    new_content = nm_dhcp_dhclient_create_config(iface,
+                                                 addr_family,
+                                                 client_id,
+                                                 send_client_id,
+                                                 anycast_address,
+                                                 hostname,
+                                                 timeout,
+                                                 use_fqdn,
+                                                 hostname_flags,
+                                                 mud_url,
+                                                 reject_servers,
+                                                 orig_path,
+                                                 orig_content,
+                                                 out_new_client_id);
+    nm_assert(new_content);
+
+    if (!g_file_set_contents(new_path, new_content, -1, &error)) {
         _LOGW("error creating dhclient configuration: %s", error->message);
-        g_clear_error(&error);
+        g_error_free(error);
+        g_free(new_path);
+        return NULL;
     }
 
-    return new;
+    return new_path;
 }
 
 static gboolean
@@ -547,11 +510,14 @@ ip4_start(NMDhcpClient *client, GError **error)
 
     client_config = nm_dhcp_client_get_config(client);
 
+    nm_assert(client_config->addr_family == AF_INET);
+
     priv->conf_file = create_dhclient_config(self,
                                              AF_INET,
                                              client_config->iface,
                                              client_config->uuid,
                                              client_config->client_id,
+                                             client_config->v4.send_client_id,
                                              client_config->anycast_address,
                                              client_config->hostname,
                                              client_config->timeout,
@@ -584,6 +550,8 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
 
     config = nm_dhcp_client_get_config(client);
 
+    nm_assert(config->addr_family == AF_INET6);
+
     if (config->v6.iaid_explicit)
         _LOGW("dhclient does not support specifying an IAID for DHCPv6, it will be ignored");
 
@@ -592,6 +560,7 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
                                              config->iface,
                                              config->uuid,
                                              NULL,
+                                             TRUE,
                                              config->anycast_address,
                                              config->hostname,
                                              config->timeout,
diff --git a/src/core/dhcp/nm-dhcp-dhcpcd.c b/src/core/dhcp/nm-dhcp-dhcpcd.c
index 8d2f928b..7c95994a 100644
--- a/src/core/dhcp/nm-dhcp-dhcpcd.c
+++ b/src/core/dhcp/nm-dhcp-dhcpcd.c
@@ -174,7 +174,7 @@ stop(NMDhcpClient *client, gboolean release)
          */
         if (kill(pid, sig) == -1) {
             errsv = errno;
-            _LOGE("failed to kill dhcpcd %d:%s", errsv, strerror(errsv));
+            _LOGE("failed to kill dhcpcd %d:%s", errsv, nm_strerror_native(errsv));
         }
 
         /* When this function exits NM expects the PID to be -1.
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index ee95abb7..9e4cedf2 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -159,7 +159,7 @@ do_connect:
                       error->message,
                       try_count,
                       (long long) (time_end - remaining_time - time_start) / 1000);
-                interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 100000);
+                interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31u))), 5000, 100000);
                 g_usleep(NM_MIN(interval, remaining_time));
                 g_clear_error(&error);
                 goto do_connect;
@@ -222,7 +222,7 @@ do_notify:
         gint64 interval;
 
         _LOGi("failure to call notify: %s (retry %u)", error->message, try_count);
-        interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000);
+        interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31u))), 5000, 25000);
         g_usleep(NM_MIN(interval, remaining_time));
         g_clear_error(&error);
         goto do_notify;
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index f36dfb4d..fc468d4c 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -1119,6 +1119,7 @@ nettools_create(NMDhcpNettools *self, GBytes **out_effective_client_id, GError *
     gs_unref_bytes GBytes                                   *client_id_new = NULL;
     const uint8_t                                           *client_id_arr;
     size_t                                                   client_id_len;
+    bool                                                     send_client_id;
     int                                                      r, fd, arp_type, transport;
     const NMDhcpClientConfig                                *client_config;
 
@@ -1150,20 +1151,37 @@ nettools_create(NMDhcpNettools *self, GBytes **out_effective_client_id, GError *
         return FALSE;
     }
 
-    /* Note that we always set a client-id. In particular for infiniband that is necessary,
-     * see https://tools.ietf.org/html/rfc4390#section-2.1 . */
-    client_id = client_config->client_id;
-    if (!client_id) {
+    client_id      = client_config->client_id;
+    send_client_id = client_config->v4.send_client_id;
+
+    if (!send_client_id && transport == N_DHCP4_TRANSPORT_INFINIBAND) {
+        /* Client-id is mandatory for infiniband: https://tools.ietf.org/html/rfc4390#section-2.1 */
+        _LOGI("ipv4.client-id is set to \"none\", but it's mandatory for Infiniband. Setting a "
+              "default one.");
+        send_client_id = TRUE;
+    }
+
+    if (send_client_id && !client_id) {
         client_id_new = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_arr, hwaddr_len);
         client_id     = client_id_new;
     }
 
-    if (!(client_id_arr = g_bytes_get_data(client_id, &client_id_len)) || client_id_len < 2) {
-        /* invalid client-ids are not expected. */
-        nm_assert_not_reached();
+    g_return_val_if_fail(client_id || !send_client_id, FALSE);
 
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id");
-        return FALSE;
+    if (send_client_id) {
+        client_id_arr = g_bytes_get_data(client_id, &client_id_len);
+        client_id_len = NM_MIN(client_id_len, 1 + _NM_MAX_CLIENT_ID_LEN);
+
+        if (!client_id_arr || client_id_len < 2) {
+            /* invalid client-ids are not expected. */
+            nm_assert_not_reached();
+            nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id");
+            return FALSE;
+        }
+    } else {
+        client_id_arr = NULL;
+        /* This will unset the client-id and prevent it from being sent */
+        client_id_len = 0;
     }
 
     r = n_dhcp4_client_config_new(&config);
@@ -1177,9 +1195,7 @@ nettools_create(NMDhcpNettools *self, GBytes **out_effective_client_id, GError *
     n_dhcp4_client_config_set_mac(config, hwaddr_arr, hwaddr_len);
     n_dhcp4_client_config_set_broadcast_mac(config, bcast_hwaddr_arr, bcast_hwaddr_len);
     n_dhcp4_client_config_set_request_broadcast(config, client_config->v4.request_broadcast);
-    r = n_dhcp4_client_config_set_client_id(config,
-                                            client_id_arr,
-                                            NM_MIN(client_id_len, 1 + _NM_MAX_CLIENT_ID_LEN));
+    r = n_dhcp4_client_config_set_client_id(config, client_id_arr, client_id_len);
     if (r) {
         set_error_nettools(error, r, "failed to set client-id");
         return FALSE;
diff --git a/src/core/dhcp/tests/test-dhcp-dhclient.c b/src/core/dhcp/tests/test-dhcp-dhclient.c
index e95f218e..0edcc296 100644
--- a/src/core/dhcp/tests/test-dhcp-dhclient.c
+++ b/src/core/dhcp/tests/test-dhcp-dhclient.c
@@ -36,11 +36,14 @@ test_config(const char         *orig,
             const char         *anycast_addr,
             const char         *mud_url)
 {
-    gs_free char *new                    = NULL;
-    gs_unref_bytes GBytes *client_id     = NULL;
-    gs_unref_bytes GBytes *new_client_id = NULL;
-
-    if (dhcp_client_id) {
+    gs_free char *new                     = NULL;
+    gs_unref_bytes GBytes *client_id      = NULL;
+    gs_unref_bytes GBytes *new_client_id  = NULL;
+    gboolean               send_client_id = TRUE;
+
+    if (nm_streq0(dhcp_client_id, "none")) {
+        send_client_id = FALSE;
+    } else if (dhcp_client_id) {
         client_id = nm_dhcp_utils_client_id_string_to_bytes(dhcp_client_id);
         g_assert(client_id);
     }
@@ -48,6 +51,7 @@ test_config(const char         *orig,
     new = nm_dhcp_dhclient_create_config(iface,
                                          addr_family,
                                          client_id,
+                                         send_client_id,
                                          anycast_addr,
                                          hostname,
                                          timeout,
@@ -502,6 +506,90 @@ test_existing_ascii_client_id(void)
                 NULL,
                 NULL);
 }
+
+/*****************************************************************************/
+
+static const char *none_client_id_orig = "send dhcp-client-identifier 10:30:04:20:7A:08;\n";
+
+static const char *none_client_id_expected =
+    "# Created by NetworkManager\n"
+    "# Merged from /path/to/dhclient.conf\n"
+    "\n"
+    "send dhcp-client-identifier \"\"; # added by NetworkManager\n"
+    "\n"
+    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
+    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
+    "option wpad code 252 = string;\n"
+    "\n"
+    "also request rfc3442-classless-static-routes;\n"
+    "also request ms-classless-static-routes;\n"
+    "also request static-routes;\n"
+    "also request wpad;\n"
+    "also request ntp-servers;\n"
+    "also request root-path;\n"
+    "\n";
+
+static void
+test_none_client_id(void)
+{
+    const char            *connection_client_id = "none";
+    gs_unref_bytes GBytes *expected_client_id   = NULL;
+
+    test_config(none_client_id_orig,
+                none_client_id_expected,
+                AF_INET,
+                NULL,
+                0,
+                FALSE,
+                NM_DHCP_HOSTNAME_FLAG_NONE,
+                connection_client_id,
+                expected_client_id,
+                "eth0",
+                NULL,
+                NULL);
+}
+
+/*****************************************************************************/
+
+static const char *missing_client_id_orig = "";
+
+static const char *missing_client_id_expected =
+    "# Created by NetworkManager\n"
+    "# Merged from /path/to/dhclient.conf\n"
+    "\n"
+    "\n"
+    "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n"
+    "option ms-classless-static-routes code 249 = array of unsigned integer 8;\n"
+    "option wpad code 252 = string;\n"
+    "\n"
+    "also request rfc3442-classless-static-routes;\n"
+    "also request ms-classless-static-routes;\n"
+    "also request static-routes;\n"
+    "also request wpad;\n"
+    "also request ntp-servers;\n"
+    "also request root-path;\n"
+    "\n";
+
+static void
+test_missing_client_id(void)
+{
+    const char            *connection_client_id = NULL;
+    gs_unref_bytes GBytes *expected_client_id   = NULL;
+
+    test_config(missing_client_id_orig,
+                missing_client_id_expected,
+                AF_INET,
+                NULL,
+                0,
+                FALSE,
+                NM_DHCP_HOSTNAME_FLAG_NONE,
+                connection_client_id,
+                expected_client_id,
+                "eth0",
+                NULL,
+                NULL);
+}
+
 /*****************************************************************************/
 
 static const char *fqdn_expected =
@@ -1364,6 +1452,8 @@ main(int argc, char **argv)
     g_test_add_func("/dhcp/dhclient/existing-hex-client-id", test_existing_hex_client_id);
     g_test_add_func("/dhcp/dhclient/existing-client-id", test_existing_escaped_client_id);
     g_test_add_func("/dhcp/dhclient/existing-ascii-client-id", test_existing_ascii_client_id);
+    g_test_add_func("/dhcp/dhclient/none-client-id", test_none_client_id);
+    g_test_add_func("/dhcp/dhclient/missing-client-id", test_missing_client_id);
     g_test_add_func("/dhcp/dhclient/fqdn", test_fqdn);
     g_test_add_func("/dhcp/dhclient/fqdn_options_override", test_fqdn_options_override);
     g_test_add_func("/dhcp/dhclient/override_hostname", test_override_hostname);
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 53564693..8f87fec1 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -543,7 +543,7 @@ add_string_item(GPtrArray *array, const char *str, gboolean dup)
 static void
 add_dns_option_item(GPtrArray *array, const char *str)
 {
-    if (_nm_utils_dns_option_find_idx(array, str) < 0)
+    if (_nm_utils_dns_option_find_idx((const char *const *) array->pdata, array->len, str) < 0)
         g_ptr_array_add(array, g_strdup(str));
 }
 
@@ -1876,8 +1876,11 @@ plugin_skip:;
         nameservers    = g_new0(char *, 2);
         nameservers[0] = g_strdup(lladdr);
 
-        need_edns0 = nm_strv_find_first(options, -1, NM_SETTING_DNS_OPTION_EDNS0) < 0;
-        need_trust = nm_strv_find_first(options, -1, NM_SETTING_DNS_OPTION_TRUST_AD) < 0;
+        need_edns0 = !nm_strv_contains(options, -1, NM_SETTING_DNS_OPTION_EDNS0)
+                     && !nm_strv_contains(options, -1, NM_SETTING_DNS_OPTION_INTERNAL_NO_ADD_EDNS0);
+        need_trust =
+            !nm_strv_contains(options, -1, NM_SETTING_DNS_OPTION_TRUST_AD)
+            && !nm_strv_contains(options, -1, NM_SETTING_DNS_OPTION_INTERNAL_NO_ADD_TRUST_AD);
 
         if (need_edns0 || need_trust) {
             gsize len;
@@ -1892,6 +1895,23 @@ plugin_skip:;
         }
     }
 
+    if (options) {
+        guint i;
+        guint j;
+
+        /* Skip internal options, those starting with '_' */
+        for (i = 0, j = 0; options[i]; i++) {
+            if (options[i][0] == '_') {
+                g_free(options[i]);
+                continue;
+            }
+            if (i != j)
+                options[j] = options[i];
+            j++;
+        }
+        options[j] = NULL;
+    }
+
     if (do_update) {
         switch (priv->rc_manager) {
         case NM_DNS_MANAGER_RESOLV_CONF_MAN_SYMLINK:
@@ -1948,7 +1968,7 @@ plugin_skip:;
     }
 
     /* signal that DNS resolution configs were changed */
-    if ((do_update || caching || force_emit) && result == SR_SUCCESS)
+    if ((caching || force_emit) && result == SR_SUCCESS)
         g_signal_emit(self, signals[CONFIG_CHANGED], 0);
 
     nm_clear_pointer(&priv->config_variant, g_variant_unref);
@@ -1964,6 +1984,16 @@ plugin_skip:;
     return TRUE;
 }
 
+gboolean
+nm_dns_manager_is_unmanaged(NMDnsManager *self)
+{
+    NMDnsManagerPrivate *priv = NM_DNS_MANAGER_GET_PRIVATE(self);
+
+    return NM_IN_SET(priv->rc_manager,
+                     NM_DNS_MANAGER_RESOLV_CONF_MAN_UNMANAGED,
+                     NM_DNS_MANAGER_RESOLV_CONF_MAN_IMMUTABLE);
+}
+
 /*****************************************************************************/
 
 gboolean
@@ -2391,7 +2421,7 @@ _resolvconf_resolved_managed(void)
          * We want to handle that, because systemd-resolved might not
          * have started yet. */
         full_path = g_file_read_link(_PATH_RESCONF, NULL);
-        if (nm_strv_find_first(RESOLVED_PATHS, G_N_ELEMENTS(RESOLVED_PATHS), full_path) >= 0)
+        if (nm_strv_contains(RESOLVED_PATHS, G_N_ELEMENTS(RESOLVED_PATHS), full_path))
             return TRUE;
 
         /* see if resolv.conf is a symlink that resolves exactly one
@@ -2403,7 +2433,7 @@ _resolvconf_resolved_managed(void)
          * We want to handle that, because systemd-resolved might not
          * have started yet. */
         real_path = realpath(_PATH_RESCONF, NULL);
-        if (nm_strv_find_first(RESOLVED_PATHS, G_N_ELEMENTS(RESOLVED_PATHS), real_path) >= 0)
+        if (nm_strv_contains(RESOLVED_PATHS, G_N_ELEMENTS(RESOLVED_PATHS), real_path))
             return TRUE;
 
         /* fall-through and resolve the symlink, to check the file
diff --git a/src/core/dns/nm-dns-manager.h b/src/core/dns/nm-dns-manager.h
index 42f9dec5..9b8c2972 100644
--- a/src/core/dns/nm-dns-manager.h
+++ b/src/core/dns/nm-dns-manager.h
@@ -148,4 +148,6 @@ char *nmtst_dns_create_resolv_conf(const char *const *searches,
                                    const char *const *nameservers,
                                    const char *const *options);
 
+gboolean nm_dns_manager_is_unmanaged(NMDnsManager *self);
+
 #endif /* __NETWORKMANAGER_DNS_MANAGER_H__ */
diff --git a/src/core/dnsmasq/nm-dnsmasq-utils.c b/src/core/dnsmasq/nm-dnsmasq-utils.c
index 69424d74..8d15c2e3 100644
--- a/src/core/dnsmasq/nm-dnsmasq-utils.c
+++ b/src/core/dnsmasq/nm-dnsmasq-utils.c
@@ -83,12 +83,12 @@ nm_dnsmasq_utils_get_range(const NMPlatformIP4Address *addr,
     mid = (host & netmask) | (((first + last) / 2) & ~netmask);
     if (host > mid) {
         /* use lower range */
-        reserved = NM_MIN(((host - first) / 10), 8);
+        reserved = NM_MIN(((host - first) / 10u), 8u);
         last     = host - 1 - reserved;
         first    = NM_MAX(first, last > NUM ? last - NUM : 0);
     } else {
         /* use upper range */
-        reserved = NM_MIN(((last - host) / 10), 8);
+        reserved = NM_MIN(((last - host) / 10u), 8u);
         first    = host + 1 + reserved;
         last     = NM_MIN(last, first < 0xFFFFFFFF - NUM ? first + NUM : 0xFFFFFFFF);
     }
diff --git a/src/core/main-utils.c b/src/core/main-utils.c
index 9360d688..0f62da29 100644
--- a/src/core/main-utils.c
+++ b/src/core/main-utils.c
@@ -192,10 +192,7 @@ nm_main_utils_ensure_not_running_pidfile(const char *pidfile)
     if (strcmp(process_name, prgname) == 0) {
         /* Check that the process exists */
         if (kill(pid, 0) == 0) {
-            fprintf(stderr,
-                    _("%s is already running (pid %" G_GINT64_FORMAT ")\n"),
-                    prgname,
-                    (gint64) pid);
+            fprintf(stderr, _("%s is already running (pid %lld)\n"), prgname, (long long) pid);
             exit(1);
         }
     }
diff --git a/src/core/meson.build b/src/core/meson.build
index 6c1d463e..45b068a6 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -102,6 +102,7 @@ libNetworkManager = static_library(
     'devices/nm-device-ethernet-utils.c',
     'devices/nm-device-factory.c',
     'devices/nm-device-generic.c',
+    'devices/nm-device-hsr.c',
     'devices/nm-device-infiniband.c',
     'devices/nm-device-ip-tunnel.c',
     'devices/nm-device-loopback.c',
diff --git a/src/core/ndisc/nm-fake-ndisc.c b/src/core/ndisc/nm-fake-ndisc.c
index f305e771..f0b6dcf6 100644
--- a/src/core/ndisc/nm-fake-ndisc.c
+++ b/src/core/ndisc/nm-fake-ndisc.c
@@ -239,7 +239,7 @@ receive_ra(gpointer user_data)
     priv->receive_ra_id = 0;
 
     /* preserve the "most managed" level  on updates. */
-    dhcp_level = MAX(rdata->public.dhcp_level, ra->dhcp_level);
+    dhcp_level = NM_MAX(rdata->public.dhcp_level, ra->dhcp_level);
 
     if (rdata->public.dhcp_level != dhcp_level) {
         rdata->public.dhcp_level = dhcp_level;
diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c
index 63dde5a5..932366ff 100644
--- a/src/core/ndisc/nm-lndp-ndisc.c
+++ b/src/core/ndisc/nm-lndp-ndisc.c
@@ -161,7 +161,7 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
          * let's keep the "most managed" level. */
         G_STATIC_ASSERT_EXPR(NM_NDISC_DHCP_LEVEL_MANAGED > NM_NDISC_DHCP_LEVEL_OTHERCONF);
         G_STATIC_ASSERT_EXPR(NM_NDISC_DHCP_LEVEL_OTHERCONF > NM_NDISC_DHCP_LEVEL_NONE);
-        dhcp_level = MAX(dhcp_level, rdata->public.dhcp_level);
+        dhcp_level = NM_MAX(dhcp_level, rdata->public.dhcp_level);
 
         if (dhcp_level != rdata->public.dhcp_level) {
             rdata->public.dhcp_level = dhcp_level;
diff --git a/src/core/ndisc/nm-ndisc.c b/src/core/ndisc/nm-ndisc.c
index 20dd2121..c8f7ed0c 100644
--- a/src/core/ndisc/nm-ndisc.c
+++ b/src/core/ndisc/nm-ndisc.c
@@ -1293,10 +1293,8 @@ nm_ndisc_dad_failed(NMNDisc *ndisc, GArray *addresses, gboolean emit_changed_sig
             NMNDiscAddress *item = &nm_g_array_index(rdata->addresses, NMNDiscAddress, j);
 
             if (IN6_ARE_ADDR_EQUAL(&item->address, addr)) {
-                char sbuf[NM_INET_ADDRSTRLEN];
-
-                _LOGI("DAD failed for discovered address %s", nm_inet6_ntop(addr, sbuf));
                 changed = TRUE;
+
                 if (!complete_address(ndisc, item)) {
                     g_array_remove_index(rdata->addresses, j);
                     continue;
diff --git a/src/core/nm-act-request.c b/src/core/nm-act-request.c
index dce18ba4..bed7ffde 100644
--- a/src/core/nm-act-request.c
+++ b/src/core/nm-act-request.c
@@ -259,7 +259,8 @@ static void
 device_state_changed(NMActiveConnection *active,
                      NMDevice           *device,
                      NMDeviceState       new_state,
-                     NMDeviceState       old_state)
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
 {
     NMActiveConnectionState       cur_ac_state    = nm_active_connection_get_state(active);
     NMActiveConnectionState       ac_state        = NM_ACTIVE_CONNECTION_STATE_UNKNOWN;
@@ -319,14 +320,20 @@ device_state_changed(NMActiveConnection *active,
                          active);
         break;
     case NM_DEVICE_STATE_DEACTIVATING:
+        if (reason == NM_DEVICE_STATE_REASON_USER_REQUESTED)
+            ac_state_reason = NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED;
+
         ac_state = NM_ACTIVE_CONNECTION_STATE_DEACTIVATING;
         break;
     case NM_DEVICE_STATE_FAILED:
     case NM_DEVICE_STATE_DISCONNECTED:
     case NM_DEVICE_STATE_UNMANAGED:
     case NM_DEVICE_STATE_UNAVAILABLE:
-        ac_state        = NM_ACTIVE_CONNECTION_STATE_DEACTIVATED;
-        ac_state_reason = NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED;
+        ac_state = NM_ACTIVE_CONNECTION_STATE_DEACTIVATED;
+        if (reason == NM_DEVICE_STATE_REASON_USER_REQUESTED)
+            ac_state_reason = NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED;
+        else
+            ac_state_reason = NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED;
 
         g_signal_handlers_disconnect_by_func(device, G_CALLBACK(device_notify), active);
         break;
diff --git a/src/core/nm-active-connection.c b/src/core/nm-active-connection.c
index 36a11f71..eb7b1cca 100644
--- a/src/core/nm-active-connection.c
+++ b/src/core/nm-active-connection.c
@@ -636,7 +636,8 @@ device_state_changed(NMDevice           *device,
         NM_ACTIVE_CONNECTION_GET_CLASS(self)->device_state_changed(self,
                                                                    device,
                                                                    new_state,
-                                                                   old_state);
+                                                                   old_state,
+                                                                   reason);
 }
 
 static void
@@ -1604,9 +1605,11 @@ static const NMDBusInterfaceInfoExtended interface_info_active_connection = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Controller",
                                                            "o",
                                                            NM_ACTIVE_CONNECTION_CONTROLLER),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Master",
-                                                           "o",
-                                                           NM_ACTIVE_CONNECTION_MASTER), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Master",
+                "o",
+                NM_ACTIVE_CONNECTION_MASTER,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
diff --git a/src/core/nm-active-connection.h b/src/core/nm-active-connection.h
index 15db68c3..8032294f 100644
--- a/src/core/nm-active-connection.h
+++ b/src/core/nm-active-connection.h
@@ -78,7 +78,8 @@ typedef struct {
     void (*device_state_changed)(NMActiveConnection *connection,
                                  NMDevice           *device,
                                  NMDeviceState       new_state,
-                                 NMDeviceState       old_state);
+                                 NMDeviceState       old_state,
+                                 NMDeviceStateReason reason);
     void (*master_failed)(NMActiveConnection *connection);
 
     void (*device_changed)(NMActiveConnection *connection,
diff --git a/src/core/nm-auth-utils.c b/src/core/nm-auth-utils.c
index 7739f443..aa2547b3 100644
--- a/src/core/nm-auth-utils.c
+++ b/src/core/nm-auth-utils.c
@@ -8,6 +8,7 @@
 #include "nm-auth-utils.h"
 
 #include "libnm-glib-aux/nm-c-list.h"
+#include "libnm-core-intern/nm-core-internal.h"
 #include "nm-setting-connection.h"
 #include "libnm-core-aux-intern/nm-auth-subject.h"
 #include "nm-auth-manager.h"
@@ -603,7 +604,6 @@ gboolean
 nm_auth_is_subject_in_acl(NMConnection *connection, NMAuthSubject *subject, char **out_error_desc)
 {
     NMSettingConnection *s_con;
-    gs_free char        *user = NULL;
     gulong               uid;
 
     g_return_val_if_fail(connection, FALSE);
@@ -621,13 +621,6 @@ nm_auth_is_subject_in_acl(NMConnection *connection, NMAuthSubject *subject, char
     if (0 == uid)
         return TRUE;
 
-    user = nm_utils_uid_to_name(uid);
-    if (!user) {
-        NM_SET_OUT(out_error_desc,
-                   g_strdup_printf("Could not determine username for uid %lu", uid));
-        return FALSE;
-    }
-
     s_con = nm_connection_get_setting_connection(connection);
     if (!s_con) {
         /* This can only happen when called from AddAndActivate, so we know
@@ -637,7 +630,7 @@ nm_auth_is_subject_in_acl(NMConnection *connection, NMAuthSubject *subject, char
     }
 
     /* Match the username returned by the session check to a user in the ACL */
-    if (!nm_setting_connection_permissions_user_allowed(s_con, user)) {
+    if (!nm_setting_connection_permissions_user_allowed_by_uid(s_con, uid)) {
         NM_SET_OUT(out_error_desc,
                    g_strdup_printf("uid %lu has no permission to perform this operation", uid));
         return FALSE;
diff --git a/src/core/nm-config-data.c b/src/core/nm-config-data.c
index ed6d8381..468e56b8 100644
--- a/src/core/nm-config-data.c
+++ b/src/core/nm-config-data.c
@@ -1219,7 +1219,7 @@ load_global_dns(GKeyFile *keyfile, gboolean internal)
     if (strv) {
         nm_strv_cleanup(strv, TRUE, TRUE, TRUE);
         for (i = 0, j = 0; strv[i]; i++) {
-            if (_nm_utils_dns_option_validate(strv[i], NULL, NULL, TRUE, NULL))
+            if (_nm_utils_dns_option_validate(strv[i], NULL, NULL, AF_UNSPEC, NULL))
                 strv[j++] = strv[i];
             else
                 g_free(strv[i]);
@@ -1453,7 +1453,7 @@ nm_global_dns_config_from_dbus(const GValue *value, GError **error)
             nm_strv_cleanup(strv, TRUE, TRUE, TRUE);
 
             for (i = 0, j = 0; strv && strv[i]; i++) {
-                if (_nm_utils_dns_option_validate(strv[i], NULL, NULL, TRUE, NULL))
+                if (_nm_utils_dns_option_validate(strv[i], NULL, NULL, AF_UNSPEC, NULL))
                     strv[j++] = strv[i];
                 else
                     g_free(strv[i]);
diff --git a/src/core/nm-config.c b/src/core/nm-config.c
index b8df41b7..5db4a92a 100644
--- a/src/core/nm-config.c
+++ b/src/core/nm-config.c
@@ -858,7 +858,6 @@ static const ConfigGroup config_groups[] = {
                              NM_CONFIG_KEYFILE_KEY_MAIN_NO_AUTO_DEFAULT,
                              NM_CONFIG_KEYFILE_KEY_MAIN_PLUGINS,
                              NM_CONFIG_KEYFILE_KEY_MAIN_RC_MANAGER,
-                             NM_CONFIG_KEYFILE_KEY_MAIN_SLAVES_ORDER,
                              NM_CONFIG_KEYFILE_KEY_MAIN_SYSTEMD_RESOLVED, ),
     },
     {
@@ -1129,12 +1128,12 @@ read_config(GKeyFile   *keyfile,
                     /* merge the string lists, by omitting duplicates. */
 
                     for (iter_val = old_val; iter_val && *iter_val; iter_val++) {
-                        if (last_char != '-' || nm_strv_find_first(new_val, -1, *iter_val) < 0)
+                        if (last_char != '-' || !nm_strv_contains(new_val, -1, *iter_val))
                             g_ptr_array_add(new, g_strdup(*iter_val));
                     }
                     for (iter_val = new_val; iter_val && *iter_val; iter_val++) {
                         /* don't add duplicates. That means an "option=a,b"; "option+=a,c" results in "option=a,b,c" */
-                        if (last_char == '+' && nm_strv_find_first(old_val, -1, *iter_val) < 0)
+                        if (last_char == '+' && !nm_strv_contains(old_val, -1, *iter_val))
                             g_ptr_array_add(new, *iter_val);
                         else
                             g_free(*iter_val);
@@ -3019,8 +3018,7 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
 gboolean
 nm_config_kernel_command_line_nm_debug(void)
 {
-    return (nm_strv_find_first(nm_utils_proc_cmdline_split(), -1, NM_CONFIG_KERNEL_CMDLINE_NM_DEBUG)
-            >= 0);
+    return nm_strv_contains(nm_utils_proc_cmdline_split(), -1, NM_CONFIG_KERNEL_CMDLINE_NM_DEBUG);
 }
 
 /*****************************************************************************/
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c
index 92de44f1..15dc31aa 100644
--- a/src/core/nm-connectivity.c
+++ b/src/core/nm-connectivity.c
@@ -79,9 +79,9 @@ struct _NMConnectivityCheckHandle {
         struct curl_slist *request_headers;
         struct curl_slist *hosts;
 
-        gsize response_good_cnt;
+        GSource *curl_timer;
 
-        guint curl_timer;
+        gsize response_good_cnt;
     } concheck;
 #endif
 
@@ -241,7 +241,7 @@ cb_data_complete(NMConnectivityCheckHandle *cb_data,
         curl_slist_free_all(cb_data->concheck.request_headers);
         curl_slist_free_all(cb_data->concheck.hosts);
     }
-    nm_clear_g_source(&cb_data->concheck.curl_timer);
+    nm_clear_g_source_inst(&cb_data->concheck.curl_timer);
     nm_clear_g_cancellable(&cb_data->concheck.resolve_cancellable);
 #endif
 
@@ -406,6 +406,7 @@ _con_curl_timeout_cb(gpointer user_data)
 {
     NMConnectivityCheckHandle *cb_data = user_data;
 
+    nm_clear_g_source_inst(&cb_data->concheck.curl_timer);
     _con_curl_check_connectivity(cb_data->concheck.curl_mhandle, CURL_SOCKET_TIMEOUT, 0);
     _complete_queued(cb_data->self);
     return G_SOURCE_CONTINUE;
@@ -416,9 +417,11 @@ multi_timer_cb(CURLM *multi, long timeout_msec, void *userdata)
 {
     NMConnectivityCheckHandle *cb_data = userdata;
 
-    nm_clear_g_source(&cb_data->concheck.curl_timer);
-    if (timeout_msec != -1)
-        cb_data->concheck.curl_timer = g_timeout_add(timeout_msec, _con_curl_timeout_cb, cb_data);
+    nm_clear_g_source_inst(&cb_data->concheck.curl_timer);
+    if (timeout_msec != -1) {
+        cb_data->concheck.curl_timer =
+            nm_g_timeout_add_source(timeout_msec, _con_curl_timeout_cb, cb_data);
+    }
     return 0;
 }
 
@@ -1292,7 +1295,7 @@ update_config(NMConnectivity *self, NMConfigData *config_data)
     priv->uri_valid = new_uri_valid;
 
     interval = nm_config_data_get_connectivity_interval(config_data);
-    interval = MIN(interval, (7 * 24 * 3600));
+    interval = NM_MIN(interval, (7u * 24 * 3600));
     if (priv->interval != interval) {
         priv->interval = interval;
         changed        = TRUE;
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 5442efbf..178ea3c4 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -738,19 +738,20 @@ nm_utils_kill_child_sync(pid_t       pid,
 
             if (!was_waiting) {
                 nm_log_dbg(log_domain,
-                           LOG_NAME_FMT ": waiting up to %ld milliseconds for process to terminate "
+                           LOG_NAME_FMT ": waiting up to %lu milliseconds for process to terminate "
                                         "normally after sending %s...",
                            LOG_NAME_ARGS,
-                           (long) MAX(wait_before_kill_msec, 0),
+                           (unsigned long) wait_before_kill_msec,
                            _kc_signal_to_string(sig));
                 was_waiting = TRUE;
             }
 
-            sleep_time = MIN(wait_until - now, sleep_duration_usec);
+            sleep_time = NM_MIN(wait_until - now, (gint64) sleep_duration_usec);
             if (loop_count < 20) {
                 /* At the beginning we expect the process to die fast.
                  * Limit the sleep time, the limit doubles with every iteration. */
-                sleep_time = MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
+                sleep_time =
+                    NM_MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
                 loop_count++;
             }
             g_usleep(sleep_time);
@@ -1031,17 +1032,17 @@ nm_utils_kill_process_sync(pid_t       pid,
                 loop_count =
                     0; /* reset the loop_count. Now we really expect the process to die quickly. */
             } else
-                sleep_time = MIN(wait_until_sigkill - now, sleep_duration_usec);
+                sleep_time = NM_MIN(wait_until_sigkill - now, (gint64) sleep_duration_usec);
         }
 
         if (!was_waiting) {
             if (wait_until_sigkill != 0) {
                 nm_log_dbg(log_domain,
                            LOG_NAME_PROCESS_FMT
-                           ": waiting up to %ld milliseconds for process to disappear before "
+                           ": waiting up to %lu milliseconds for process to disappear before "
                            "sending KILL signal after sending %s...",
                            LOG_NAME_ARGS,
-                           (long) wait_before_kill_msec,
+                           (unsigned long) wait_before_kill_msec,
                            _kc_signal_to_string(sig));
             } else if (max_wait_until != 0) {
                 nm_log_dbg(
@@ -1064,7 +1065,7 @@ nm_utils_kill_process_sync(pid_t       pid,
         if (loop_count < 20) {
             /* At the beginning we expect the process to die fast.
              * Limit the sleep time, the limit doubles with every iteration. */
-            sleep_time = MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
+            sleep_time = NM_MIN(sleep_time, (((guint64) 1) << loop_count) * G_USEC_PER_SEC / 2000);
             loop_count++;
         }
         g_usleep(sleep_time);
@@ -2711,7 +2712,7 @@ _host_id_read_timestamp(gboolean      use_secret_key_file,
 
 #define EPOCH_TWO_YEARS (G_GINT64_CONSTANT(2 * 365 * 24 * 3600) * NM_UTILS_NSEC_PER_SEC)
 
-    v = nm_hash_siphash42(1156657133u, host_id, host_id_len);
+    v = c_siphash_hash(NM_HASH_SEED_16_U64(1156657133u), host_id, host_id_len);
 
     now = time(NULL);
     *out_timestamp_ns =
@@ -3396,6 +3397,7 @@ nm_utils_stable_id_parse(const char *stable_id,
                          const char *hwaddr,
                          const char *bootid,
                          const char *uuid,
+                         GBytes     *ssid,
                          char      **out_generated)
 {
     nm_auto_str_buf NMStrBuf str = NM_STR_BUF_INIT_A(NM_UTILS_GET_NEXT_REALLOC_SIZE_232, FALSE);
@@ -3481,7 +3483,29 @@ nm_utils_stable_id_parse(const char *stable_id,
             _stable_id_append(&str, deviceid);
         else if (CHECK_PREFIX("${MAC}"))
             _stable_id_append(&str, hwaddr);
-        else if (g_str_has_prefix(&stable_id[i], "${RANDOM}")) {
+        else if (CHECK_PREFIX("${NETWORK_SSID}")) {
+            gs_free char *value_free = NULL;
+            gs_free char *s          = NULL;
+            const char   *value;
+            const char   *type_id;
+
+            if (ssid) {
+                type_id = "s:";
+                value   = nm_utils_buf_utf8safe_escape_bytes(ssid,
+                                                           NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL,
+                                                           &value_free);
+            } else {
+                /* If we have no SSID, we fallback to the connection's UUID.
+                 *
+                 * Give a separate prefix (@type_id), so that an SSID and a UUID
+                 * fallback never result in the same output. */
+                type_id = "c:";
+                value   = uuid ?: "";
+            }
+
+            s = g_strjoin("", type_id, value, NULL);
+            _stable_id_append(&str, s);
+        } 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".
              * It makes however sense for RFC 7217 Stable Privacy IPv6 addresses
@@ -3522,6 +3546,29 @@ nm_utils_stable_id_parse(const char *stable_id,
     return NM_UTILS_STABLE_TYPE_GENERATED;
 }
 
+NMUtilsStableType
+nm_utils_stable_id_parse_network_ssid(GBytes     *ssid,
+                                      const char *uuid,
+                                      gboolean    complete,
+                                      char      **out_stable_id)
+{
+    NMUtilsStableType stable_type;
+
+    stable_type =
+        nm_utils_stable_id_parse("${NETWORK_SSID}", NULL, NULL, NULL, uuid, ssid, out_stable_id);
+
+    nm_assert(stable_type == NM_UTILS_STABLE_TYPE_GENERATED);
+    nm_assert(!out_stable_id || nm_str_not_empty(*out_stable_id));
+
+    if (complete && out_stable_id) {
+        gs_free char *ss = g_steal_pointer(out_stable_id);
+
+        *out_stable_id = nm_utils_stable_id_generated_complete(ss);
+    }
+
+    return stable_type;
+}
+
 /*****************************************************************************/
 
 static gboolean
@@ -3567,7 +3614,7 @@ nm_utils_ipv6_addr_set_stable_privacy_with_host_id(NMUtilsStableType stable_type
 
     sum = g_checksum_new(G_CHECKSUM_SHA256);
 
-    host_id_len = MIN(host_id_len, G_MAXUINT32);
+    host_id_len = NM_MIN(host_id_len, G_MAXUINT32);
 
     if (stable_type != NM_UTILS_STABLE_TYPE_UUID) {
         guint8 stable_type_uint8;
@@ -3742,7 +3789,7 @@ _hw_addr_gen_stable_eth(NMUtilsStableType stable_type,
 
     sum = g_checksum_new(G_CHECKSUM_SHA256);
 
-    host_id_len = MIN(host_id_len, G_MAXUINT32);
+    host_id_len = NM_MIN(host_id_len, G_MAXUINT32);
 
     nm_assert(stable_type < (NMUtilsStableType) 255);
     stable_type_uint8 = stable_type;
@@ -3819,23 +3866,23 @@ nm_utils_dhcp_client_id_mac(int arp_type, const guint8 *hwaddr, gsize hwaddr_len
     return g_bytes_new_take(client_id_buf, hwaddr_len + 1);
 }
 
-#define HASH_KEY              \
-    ((const guint8[16]){0x80, \
-                        0x11, \
-                        0x8c, \
-                        0xc2, \
-                        0xfe, \
-                        0x4a, \
-                        0x03, \
-                        0xee, \
-                        0x3e, \
-                        0xd6, \
-                        0x0c, \
-                        0x6f, \
-                        0x36, \
-                        0x39, \
-                        0x14, \
-                        0x09})
+#define HASH_KEY          \
+    NM_HASH_SEED_16(0x80, \
+                    0x11, \
+                    0x8c, \
+                    0xc2, \
+                    0xfe, \
+                    0x4a, \
+                    0x03, \
+                    0xee, \
+                    0x3e, \
+                    0xd6, \
+                    0x0c, \
+                    0x6f, \
+                    0x36, \
+                    0x39, \
+                    0x14, \
+                    0x09)
 
 /**
  * nm_utils_create_dhcp_iaid:
@@ -4255,8 +4302,8 @@ read_device_factory_paths_sort_fcn(gconstpointer a, gconstpointer b)
     const struct plugin_info *db = b;
     time_t                    ta, tb;
 
-    ta = MAX(da->st.st_mtime, da->st.st_ctime);
-    tb = MAX(db->st.st_mtime, db->st.st_ctime);
+    ta = NM_MAX(da->st.st_mtime, da->st.st_ctime);
+    tb = NM_MAX(db->st.st_mtime, db->st.st_ctime);
 
     if (ta < tb)
         return 1;
@@ -4779,15 +4826,91 @@ get_max_rate_vht(const guint8 *bytes, guint len, guint32 *out_maxrate)
     return TRUE;
 }
 
+static gboolean
+get_bandwidth_ht(const guint8 *bytes, guint len, guint32 *out_bandwidth)
+{
+    guint8 ht_op_flag_group;
+
+    /* http://standards.ieee.org/getieee802/download/802.11-2012.pdf
+     * https://mrncciew.com/2014/11/04/cwap-ht-operations-ie/
+     * IEEE std 802.11-2020 section 9.4.2.56
+     */
+
+    if (len != 22)
+        return FALSE;
+
+    ht_op_flag_group = bytes[1];
+
+    /* Check bit for 20Mhz or 40Mhz */
+    if (ht_op_flag_group & (1 << 2))
+        *out_bandwidth = 40;
+    else
+        *out_bandwidth = 20;
+
+    return TRUE;
+}
+
+static gboolean
+get_bandwidth_vht(const guint8 *bytes, guint len, guint32 *out_bandwidth)
+{
+    guint8 sta_channel_width;
+    guint8 ccfs0;
+    guint8 ccfs1;
+
+    /* http://chimera.labs.oreilly.com/books/1234000001739/ch03.html#management_frames
+     * https://community.arubanetworks.com/community-home/librarydocuments/viewdocument?DocumentKey=799aad1b-d9c4-421a-a492-a111e8680d34&CommunityKey=39a6bdf4-2376-46f9-853a-49420d2d0caa&tab=librarydocuments
+     * IEEE Std 802.11-2020 section 9.4.2.158
+     */
+
+    if (len < 3)
+        return FALSE;
+
+    sta_channel_width = bytes[0];
+    ccfs0             = bytes[1];
+    ccfs1             = bytes[2];
+    switch (sta_channel_width) {
+    case 0:
+        /* we rely on HT Operation IE value*/
+        return FALSE;
+    case 1:
+        if (ccfs1 == 0)
+            *out_bandwidth = 80;
+        else if (abs(ccfs1 - ccfs0) == 8)
+            *out_bandwidth = 160;
+        else if (abs(ccfs1 - ccfs0) > 16)
+            /* we are considering 80+80 as 160 */
+            *out_bandwidth = 160;
+        else
+            /* falling back to 80 MHz */
+            *out_bandwidth = 80;
+        break;
+    case 2:
+        /* deprecated */
+        *out_bandwidth = 160;
+        break;
+    case 3:
+        /* deprecated */
+        *out_bandwidth = 160;
+        break;
+    default:
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
 /* Management Frame Information Element IDs, ieee80211_eid */
 #define WLAN_EID_HT_CAPABILITY   45
+#define WLAN_EID_HT_OPERATION    61
 #define WLAN_EID_VHT_CAPABILITY  191
+#define WLAN_EID_VHT_OPERATION   192
 #define WLAN_EID_VENDOR_SPECIFIC 221
 
 void
 nm_wifi_utils_parse_ies(const guint8 *bytes,
                         gsize         len,
                         guint32      *out_max_rate,
+                        guint32      *out_bandwidth,
                         gboolean     *out_metered,
                         gboolean     *out_owe_transition_mode)
 {
@@ -4795,6 +4918,7 @@ nm_wifi_utils_parse_ies(const guint8 *bytes,
     guint32 m;
 
     NM_SET_OUT(out_max_rate, 0);
+    NM_SET_OUT(out_bandwidth, 0);
     NM_SET_OUT(out_metered, FALSE);
     NM_SET_OUT(out_owe_transition_mode, FALSE);
 
@@ -4816,12 +4940,20 @@ nm_wifi_utils_parse_ies(const guint8 *bytes,
                     *out_max_rate = NM_MAX(*out_max_rate, m);
             }
             break;
+        case WLAN_EID_HT_OPERATION:
+            if (out_bandwidth)
+                get_bandwidth_ht(bytes, elem_len, out_bandwidth);
+            break;
         case WLAN_EID_VHT_CAPABILITY:
             if (out_max_rate) {
                 if (get_max_rate_vht(bytes, elem_len, &m))
                     *out_max_rate = NM_MAX(*out_max_rate, m);
             }
             break;
+        case WLAN_EID_VHT_OPERATION:
+            if (out_bandwidth)
+                get_bandwidth_vht(bytes, elem_len, out_bandwidth);
+            break;
         case WLAN_EID_VENDOR_SPECIFIC:
             if (len == 8 && bytes[0] == 0x00 /* OUI: Microsoft */
                 && bytes[1] == 0x50 && bytes[2] == 0xf2
@@ -5333,7 +5465,7 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
         l = (dot - hostname);
     else
         l = strlen(hostname);
-    l = MIN(l, (gsize) NM_HOST_NAME_MAX);
+    l = NM_MIN(l, (gsize) NM_HOST_NAME_MAX);
 
     s = g_strndup(hostname, l);
 
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index 55112504..f015236d 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -313,8 +313,14 @@ NMUtilsStableType nm_utils_stable_id_parse(const char *stable_id,
                                            const char *hwaddr,
                                            const char *bootid,
                                            const char *uuid,
+                                           GBytes     *ssid,
                                            char      **out_generated);
 
+NMUtilsStableType nm_utils_stable_id_parse_network_ssid(GBytes     *ssid,
+                                                        const char *uuid,
+                                                        gboolean    complete,
+                                                        char      **out_stable_id);
+
 char *nm_utils_stable_id_random(void);
 char *nm_utils_stable_id_generated_complete(const char *msg);
 
@@ -455,6 +461,7 @@ const char *nm_utils_parse_dns_domain(const char *domain, gboolean *is_routing);
 void nm_wifi_utils_parse_ies(const guint8 *bytes,
                              gsize         len,
                              guint32      *out_max_rate,
+                             guint32      *out_bandwidth,
                              gboolean     *out_metered,
                              gboolean     *out_owe_transition_mode);
 
diff --git a/src/core/nm-dbus-manager.c b/src/core/nm-dbus-manager.c
index 0bde5971..7e6757f0 100644
--- a/src/core/nm-dbus-manager.c
+++ b/src/core/nm-dbus-manager.c
@@ -1644,8 +1644,8 @@ _new_unix_process(GDBusMethodInvocation *context,
     /* polkit glib library stores uid and pid as int. There might be some
      * pitfalls if the id ever happens to be larger then that. Just assert against
      * it here. */
-    g_return_val_if_fail(uid <= MIN(G_MAXINT, G_MAXINT32), NULL);
-    g_return_val_if_fail(pid > 0 && pid <= MIN(G_MAXINT, G_MAXINT32), NULL);
+    g_return_val_if_fail(uid <= NM_MIN(G_MAXINT, G_MAXINT32), NULL);
+    g_return_val_if_fail(pid > 0 && pid <= NM_MIN(G_MAXINT, G_MAXINT32), NULL);
 
     self = nm_auth_subject_new_unix_process(dbus_sender, pid, uid);
 
diff --git a/src/core/nm-dbus-utils.c b/src/core/nm-dbus-utils.c
index 05eca84f..0a1be4a2 100644
--- a/src/core/nm-dbus-utils.c
+++ b/src/core/nm-dbus-utils.c
@@ -11,6 +11,18 @@
 
 /*****************************************************************************/
 
+const GDBusAnnotationInfo _nm_gdbus_annotation_info_deprecated = {
+    .key   = "org.freedesktop.DBus.Deprecated",
+    .value = "true",
+};
+
+const GDBusAnnotationInfo *const _nm_gdbus_annotation_info_list_deprecated[] = {
+    NM_GDBUS_ANNOTATION_INFO_DEPRECATED(),
+    NULL,
+};
+
+/*****************************************************************************/
+
 GDBusPropertyInfo *
 nm_dbus_utils_interface_info_lookup_property(const GDBusInterfaceInfo *interface_info,
                                              const char               *property_name,
diff --git a/src/core/nm-dbus-utils.h b/src/core/nm-dbus-utils.h
index 54b50634..237b37d8 100644
--- a/src/core/nm-dbus-utils.h
+++ b/src/core/nm-dbus-utils.h
@@ -45,34 +45,55 @@ typedef struct {
 G_STATIC_ASSERT(G_STRUCT_OFFSET(NMDBusPropertyInfoExtended, property_name)
                 == G_STRUCT_OFFSET(struct _NMDBusPropertyInfoExtendedBase, property_name));
 
-#define NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(m_name, m_signature, m_property_name) \
-    ((GDBusPropertyInfo *) &((const struct _NMDBusPropertyInfoExtendedBase){                 \
-        ._parent =                                                                           \
-            {                                                                                \
-                .ref_count = -1,                                                             \
-                .name      = m_name,                                                         \
-                .signature = m_signature,                                                    \
-                .flags     = G_DBUS_PROPERTY_INFO_FLAGS_READABLE,                            \
-            },                                                                               \
-        .property_name = m_property_name,                                                    \
+extern const GDBusAnnotationInfo _nm_gdbus_annotation_info_deprecated;
+
+#define NM_GDBUS_ANNOTATION_INFO_DEPRECATED() \
+    ((GDBusAnnotationInfo *) &_nm_gdbus_annotation_info_deprecated)
+
+#define NM_DEFINE_DBUS_ANNOTATION_INFO(a_key, a_value)      \
+    ((GDBusAnnotationInfo *) &((const GDBusAnnotationInfo){ \
+        .key   = (a_key),                                   \
+        .value = (a_value),                                 \
+    }))
+
+extern const GDBusAnnotationInfo *const _nm_gdbus_annotation_info_list_deprecated[];
+
+/* This is a (NULL terminated) list of GDBusAnnotationInfo with only one entry:
+ * NM_GDBUS_ANNOTATION_INFO_DEPRECATED. This single instance can be reused for the
+ * common case where we only have one annotation (and it's the deprecation).
+ *
+ * Otherwise, NM_DEFINE_DBUS_ANNOTATION_INFOS() is for defining a new list. */
+#define NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED() \
+    ((GDBusAnnotationInfo **) _nm_gdbus_annotation_info_list_deprecated)
+
+#define NM_DEFINE_DBUS_ANNOTATION_INFOS(...) \
+    ((GDBusAnnotationInfo **) ((const GDBusAnnotationInfo *const[]){__VA_ARGS__, NULL}))
+
+#define NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(m_name, m_signature, m_property_name, ...) \
+    ((GDBusPropertyInfo *) &((const struct _NMDBusPropertyInfoExtendedBase){                      \
+        ._parent       = {.ref_count = -1,                                                        \
+                          .name      = m_name,                                                    \
+                          .signature = m_signature,                                               \
+                          .flags     = G_DBUS_PROPERTY_INFO_FLAGS_READABLE,                       \
+                          __VA_ARGS__},                                                           \
+        .property_name = m_property_name,                                                         \
     }))
 
 #define NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READWRITABLE(m_name,                   \
                                                            m_signature,              \
                                                            m_property_name,          \
                                                            m_permission,             \
-                                                           m_audit_op)               \
+                                                           m_audit_op,               \
+                                                           ...)                      \
     ((GDBusPropertyInfo *) &((const struct _NMDBusPropertyInfoExtendedReadWritable){ \
         ._base =                                                                     \
             {                                                                        \
-                ._parent =                                                           \
-                    {                                                                \
-                        .ref_count = -1,                                             \
-                        .name      = m_name,                                         \
-                        .signature = m_signature,                                    \
-                        .flags     = G_DBUS_PROPERTY_INFO_FLAGS_READABLE             \
-                                 | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE,              \
-                    },                                                               \
+                ._parent       = {.ref_count = -1,                                   \
+                                  .name      = m_name,                               \
+                                  .signature = m_signature,                          \
+                                  .flags     = G_DBUS_PROPERTY_INFO_FLAGS_READABLE   \
+                                           | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE,    \
+                                  __VA_ARGS__},                                      \
                 .property_name = m_property_name,                                    \
             },                                                                       \
         .permission = m_permission,                                                  \
diff --git a/src/core/nm-dispatcher.c b/src/core/nm-dispatcher.c
index cdc07dd6..9aa4194e 100644
--- a/src/core/nm-dispatcher.c
+++ b/src/core/nm-dispatcher.c
@@ -50,6 +50,8 @@
     }                                                         \
     G_STMT_END
 
+static gboolean nm_dispatcher_need_device(NMDispatcherAction action);
+
 /*****************************************************************************/
 
 struct NMDispatcherCallId {
@@ -469,7 +471,8 @@ static const char *action_table[] = {[NM_DISPATCHER_ACTION_HOSTNAME]      = NMD_
                                      [NM_DISPATCHER_ACTION_DHCP_CHANGE_6] = NMD_ACTION_DHCP6_CHANGE,
                                      [NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE] =
                                          NMD_ACTION_CONNECTIVITY_CHANGE,
-                                     [NM_DISPATCHER_ACTION_REAPPLY] = NMD_ACTION_REAPPLY};
+                                     [NM_DISPATCHER_ACTION_REAPPLY]    = NMD_ACTION_REAPPLY,
+                                     [NM_DISPATCHER_ACTION_DNS_CHANGE] = NMD_ACTION_DNS_CHANGE};
 
 static const char *
 action_to_string(NMDispatcherAction action)
@@ -530,9 +533,7 @@ _dispatcher_call(NMDispatcherAction    action,
     if (G_UNLIKELY(!request_id))
         request_id = ++gl.request_id_counter;
 
-    /* All actions except 'hostname' and 'connectivity-change' require a device */
-    if (action == NM_DISPATCHER_ACTION_HOSTNAME
-        || action == NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE) {
+    if (!nm_dispatcher_need_device(action)) {
         _LOG2D(request_id,
                log_ifname,
                log_con_uuid,
@@ -592,9 +593,8 @@ _dispatcher_call(NMDispatcherAction    action,
     g_variant_builder_init(&vpn_ip4_props, G_VARIANT_TYPE_VARDICT);
     g_variant_builder_init(&vpn_ip6_props, G_VARIANT_TYPE_VARDICT);
 
-    /* hostname and connectivity-change actions don't send device data */
-    if (action != NM_DISPATCHER_ACTION_HOSTNAME
-        && action != NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE) {
+    /* hostname, DNS and connectivity-change actions don't send device data */
+    if (nm_dispatcher_need_device(action)) {
         fill_device_props(device,
                           &device_props,
                           &device_proxy_props,
@@ -925,6 +925,30 @@ nm_dispatcher_call_connectivity(NMConnectivityState  connectivity_state,
                             out_call_id);
 }
 
+/**
+ * nm_dispatcher_call_dns_change():
+ *
+ * This method does not block the caller.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_dns_change(void)
+{
+    return _dispatcher_call(NM_DISPATCHER_ACTION_DNS_CHANGE,
+                            FALSE,
+                            NULL,
+                            NULL,
+                            NULL,
+                            FALSE,
+                            NM_CONNECTIVITY_UNKNOWN,
+                            NULL,
+                            NULL,
+                            NULL,
+                            NULL,
+                            NULL);
+}
+
 void
 nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
 {
@@ -937,3 +961,16 @@ nm_dispatcher_call_cancel(NMDispatcherCallId *call_id)
     _LOG3D(call_id, "cancelling dispatcher callback action");
     call_id->callback = NULL;
 }
+
+/* All actions except 'hostname', 'connectivity-change' and 'dns-change' require
+ * a device */
+static gboolean
+nm_dispatcher_need_device(NMDispatcherAction action)
+{
+    if (action == NM_DISPATCHER_ACTION_HOSTNAME
+        || action == NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE
+        || action == NM_DISPATCHER_ACTION_DNS_CHANGE) {
+        return FALSE;
+    }
+    return TRUE;
+}
diff --git a/src/core/nm-dispatcher.h b/src/core/nm-dispatcher.h
index 30716399..a1cb96b7 100644
--- a/src/core/nm-dispatcher.h
+++ b/src/core/nm-dispatcher.h
@@ -23,6 +23,7 @@ typedef enum {
     NM_DISPATCHER_ACTION_DHCP_CHANGE_6,
     NM_DISPATCHER_ACTION_CONNECTIVITY_CHANGE,
     NM_DISPATCHER_ACTION_REAPPLY,
+    NM_DISPATCHER_ACTION_DNS_CHANGE,
 } NMDispatcherAction;
 
 #define NM_DISPATCHER_ACTION_DHCP_CHANGE_X(IS_IPv4) \
@@ -69,6 +70,8 @@ gboolean nm_dispatcher_call_connectivity(NMConnectivityState  state,
                                          gpointer             user_data,
                                          NMDispatcherCallId **out_call_id);
 
+gboolean nm_dispatcher_call_dns_change(void);
+
 void nm_dispatcher_call_cancel(NMDispatcherCallId *call_id);
 
 #endif /* __NM_DISPATCHER_H__ */
diff --git a/src/core/nm-ip-config.c b/src/core/nm-ip-config.c
index bec67d9a..c4dc04da 100644
--- a/src/core/nm-ip-config.c
+++ b/src/core/nm-ip-config.c
@@ -16,6 +16,10 @@
 
 /*****************************************************************************/
 
+#define NOTIFY_PLATFORM_RATELIMIT_MSEC 333
+
+/*****************************************************************************/
+
 GType nm_ip4_config_get_type(void);
 GType nm_ip6_config_get_type(void);
 
@@ -68,6 +72,73 @@ _value_set_variant_as(GValue *value, const char *const *strv, guint len)
 /*****************************************************************************/
 
 static void
+_notify_platform_handle(NMIPConfig *self, gint64 now_msec)
+{
+    NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
+    guint32            obj_type_flags;
+
+    nm_clear_g_source_inst(&priv->notify_platform_timeout_source);
+
+    priv->notify_platform_rlimited_until_msec = now_msec + NOTIFY_PLATFORM_RATELIMIT_MSEC;
+
+    obj_type_flags = nm_steal_int(&priv->notify_platform_obj_type_flags);
+
+    nm_assert(obj_type_flags != 0u);
+
+    _handle_platform_change(self, obj_type_flags, FALSE);
+}
+
+static gboolean
+_notify_platform_cb(gpointer user_data)
+{
+    _notify_platform_handle(user_data, nm_utils_get_monotonic_timestamp_msec());
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_notify_platform(NMIPConfig *self, guint32 obj_type_flags)
+{
+    const int          addr_family = nm_ip_config_get_addr_family(self);
+    const int          IS_IPv4     = NM_IS_IPv4(addr_family);
+    NMIPConfigPrivate *priv        = NM_IP_CONFIG_GET_PRIVATE(self);
+    gint64             now_msec;
+
+    obj_type_flags &= (nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ADDRESS(IS_IPv4))
+                       | nmp_object_type_to_flags(NMP_OBJECT_TYPE_IP_ROUTE(IS_IPv4)));
+
+    if (obj_type_flags == 0u)
+        return;
+
+    priv->notify_platform_obj_type_flags |= obj_type_flags;
+
+    if (priv->notify_platform_timeout_source) {
+        /* We are currently rate limited. Don't bother to check whether
+         * (now_msec < priv->notify_platform_rlimited_until_msec), just always
+         * delegate to the timeout handler. It is scheduled with a lower idle
+         * priority, so we want that additional backoff. */
+        return;
+    }
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    if (now_msec < priv->notify_platform_rlimited_until_msec) {
+        priv->notify_platform_timeout_source = nm_g_source_attach(
+            /* Schedule with a low G_PRIORITY_LOW. */
+            nm_g_timeout_source_new(priv->notify_platform_rlimited_until_msec - now_msec,
+                                    G_PRIORITY_LOW - 10,
+                                    _notify_platform_cb,
+                                    self,
+                                    NULL),
+            NULL);
+        return;
+    }
+
+    _notify_platform_handle(self, now_msec);
+}
+
+/*****************************************************************************/
+
+static void
 _l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMIPConfig *self)
 {
     switch (notify_data->notify_type) {
@@ -76,7 +147,7 @@ _l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMIPCo
             _handle_l3cd_changed(self, notify_data->l3cd_changed.l3cd_new);
         break;
     case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
-        _handle_platform_change(self, notify_data->platform_change_on_idle.obj_type_flags, FALSE);
+        _notify_platform(self, notify_data->platform_change_on_idle.obj_type_flags);
         break;
     default:
         break;
@@ -207,6 +278,8 @@ finalize(GObject *object)
     NMIPConfig        *self = NM_IP_CONFIG(object);
     NMIPConfigPrivate *priv = NM_IP_CONFIG_GET_PRIVATE(self);
 
+    nm_clear_g_source_inst(&priv->notify_platform_timeout_source);
+
     nm_clear_g_signal_handler(priv->l3cfg, &priv->l3cfg_notify_id);
 
     g_object_unref(priv->l3cfg);
@@ -412,23 +485,31 @@ static const NMDBusInterfaceInfoExtended interface_info_ip4_config = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_IP4_CONFIG,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Addresses",
-                                                           "aau",
-                                                           NM_IP4_CONFIG_ADDRESSES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Addresses",
+                "aau",
+                NM_IP4_CONFIG_ADDRESSES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ADDRESS_DATA),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Routes", "aau", NM_IP4_CONFIG_ROUTES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Routes",
+                "aau",
+                NM_IP4_CONFIG_ROUTES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RouteData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ROUTE_DATA),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("NameserverData",
                                                            "aa{sv}",
                                                            NM_IP4_CONFIG_NAMESERVER_DATA),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Nameservers",
-                                                           "au",
-                                                           NM_IP4_CONFIG_NAMESERVERS),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Nameservers",
+                "au",
+                NM_IP4_CONFIG_NAMESERVERS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Domains", "as", NM_IP_CONFIG_DOMAINS),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Searches", "as", NM_IP_CONFIG_SEARCHES),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("DnsOptions",
@@ -440,9 +521,11 @@ static const NMDBusInterfaceInfoExtended interface_info_ip4_config = {
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WinsServerData",
                                                            "as",
                                                            NM_IP4_CONFIG_WINS_SERVER_DATA),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WinsServers",
-                                                           "au",
-                                                           NM_IP4_CONFIG_WINS_SERVERS), ), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "WinsServers",
+                "au",
+                NM_IP4_CONFIG_WINS_SERVERS,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ), ), ),
 };
 
 static void
@@ -541,16 +624,20 @@ static const NMDBusInterfaceInfoExtended interface_info_ip6_config = {
     .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
         NM_DBUS_INTERFACE_IP6_CONFIG,
         .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Addresses",
-                                                           "a(ayuay)",
-                                                           NM_IP6_CONFIG_ADDRESSES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Addresses",
+                "a(ayuay)",
+                NM_IP6_CONFIG_ADDRESSES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("AddressData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ADDRESS_DATA),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Gateway", "s", NM_IP_CONFIG_GATEWAY),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("Routes",
-                                                           "a(ayuayu)",
-                                                           NM_IP6_CONFIG_ROUTES),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "Routes",
+                "a(ayuayu)",
+                NM_IP6_CONFIG_ROUTES,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RouteData",
                                                            "aa{sv}",
                                                            NM_IP_CONFIG_ROUTE_DATA),
diff --git a/src/core/nm-ip-config.h b/src/core/nm-ip-config.h
index 0dcea83b..47a40bd2 100644
--- a/src/core/nm-ip-config.h
+++ b/src/core/nm-ip-config.h
@@ -32,7 +32,10 @@ struct _NMIPConfigPrivate {
     struct {
         const NMPObject *best_default_route;
     } v_gateway;
-    gulong l3cfg_notify_id;
+    GSource *notify_platform_timeout_source;
+    gint64   notify_platform_rlimited_until_msec;
+    gulong   l3cfg_notify_id;
+    guint32  notify_platform_obj_type_flags;
 };
 
 struct _NMIPConfig {
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 96274ba9..a4647116 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -157,6 +157,8 @@ struct _NML3ConfigData {
     bool has_routes_with_type_local_6_set : 1;
     bool has_routes_with_type_local_4_val : 1;
     bool has_routes_with_type_local_6_val : 1;
+    bool dhcp_enabled_4 : 1;
+    bool dhcp_enabled_6 : 1;
 
     bool ndisc_hop_limit_set : 1;
     bool ndisc_reachable_time_msec_set : 1;
@@ -324,7 +326,7 @@ _strv_ptrarray_merge(GPtrArray **p_dst, const GPtrArray *src)
         const char *s = src->pdata[i];
 
         if (dst_initial_len > 0
-            && nm_strv_find_first((const char *const *) ((*p_dst)->pdata), dst_initial_len, s) >= 0)
+            && nm_strv_contains((const char *const *) ((*p_dst)->pdata), dst_initial_len, s))
             continue;
 
         g_ptr_array_add(*p_dst, g_strdup(s));
@@ -1642,7 +1644,7 @@ nm_l3_config_data_get_dns_priority(const NML3ConfigData *self, int addr_family,
     case AF_UNSPEC:
         if (NM_FLAGS_ANY(self->flags, NM_L3_CONFIG_DAT_FLAGS_HAS_DNS_PRIORITY_4)) {
             if (NM_FLAGS_ANY(self->flags, NM_L3_CONFIG_DAT_FLAGS_HAS_DNS_PRIORITY_6)) {
-                NM_SET_OUT(out_prio, MIN(self->dns_priority_4, self->dns_priority_6));
+                NM_SET_OUT(out_prio, NM_MIN(self->dns_priority_4, self->dns_priority_6));
                 return TRUE;
             }
             NM_SET_OUT(out_prio, self->dns_priority_4);
@@ -1933,6 +1935,19 @@ nm_l3_config_data_set_mptcp_flags(NML3ConfigData *self, NMMptcpFlags mptcp_flags
     return TRUE;
 }
 
+gboolean
+nm_l3_config_data_get_dhcp_enabled(const NML3ConfigData *self, int addr_family)
+{
+    const int IS_IPv4 = NM_IS_IPv4(addr_family);
+
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+    if (IS_IPv4) {
+        return self->dhcp_enabled_4;
+    } else {
+        return self->dhcp_enabled_6;
+    }
+}
+
 NMProxyConfigMethod
 nm_l3_config_data_get_proxy_method(const NML3ConfigData *self)
 {
@@ -2231,14 +2246,23 @@ static const NML3ConfigData *
 get_empty_l3cd(void)
 {
     static NML3ConfigData *empty_l3cd;
+    NML3ConfigData        *l3cd;
+
+again:
+    l3cd = g_atomic_pointer_get(&empty_l3cd);
+    if (G_UNLIKELY(!l3cd)) {
+        l3cd = nm_l3_config_data_new(nm_dedup_multi_index_new(), 1, NM_IP_CONFIG_SOURCE_UNKNOWN);
+        l3cd->ifindex = 0;
 
-    if (!empty_l3cd) {
-        empty_l3cd =
-            nm_l3_config_data_new(nm_dedup_multi_index_new(), 1, NM_IP_CONFIG_SOURCE_UNKNOWN);
-        empty_l3cd->ifindex = 0;
+        nm_l3_config_data_seal(l3cd);
+
+        if (!g_atomic_pointer_compare_and_exchange(&empty_l3cd, NULL, l3cd)) {
+            nm_l3_config_data_unref(l3cd);
+            goto again;
+        }
     }
 
-    return empty_l3cd;
+    return l3cd;
 }
 
 int
@@ -2713,6 +2737,7 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
     guint              nnameservers;
     guint              nsearches;
     const char        *gateway_str;
+    const char        *method;
     NMIPAddr           gateway_bin;
     guint              i;
     int                idx;
@@ -2730,6 +2755,24 @@ _init_from_connection_ip(NML3ConfigData *self, int addr_family, NMConnection *co
 
     never_default = nm_setting_ip_config_get_never_default(s_ip);
 
+    method = nm_setting_ip_config_get_method(s_ip);
+    if (IS_IPv4) {
+        if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
+            self->dhcp_enabled_4 = TRUE;
+        } else {
+            self->dhcp_enabled_4 = FALSE;
+        }
+    } else {
+        method = nm_setting_ip_config_get_method(s_ip);
+        if (NM_IN_STRSET(method,
+                         NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                         NM_SETTING_IP6_CONFIG_METHOD_DHCP)) {
+            self->dhcp_enabled_6 = TRUE;
+        } else {
+            self->dhcp_enabled_6 = FALSE;
+        }
+    }
+
     nm_l3_config_data_set_never_default(self, addr_family, !!never_default);
 
     if (!never_default && (gateway_str = nm_setting_ip_config_get_gateway(s_ip))
@@ -3413,6 +3456,11 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
         self->dhcp_lease_x[0] = nm_dhcp_lease_ref(self->dhcp_lease_x[0]);
         self->dhcp_lease_x[1] = nm_dhcp_lease_ref(self->dhcp_lease_x[1]);
     }
+    if (src->dhcp_enabled_4)
+        self->dhcp_enabled_4 = TRUE;
+
+    if (src->dhcp_enabled_6)
+        self->dhcp_enabled_6 = TRUE;
 }
 
 NML3ConfigData *
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index 80abb00d..b55b2f41 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -554,6 +554,8 @@ NMSettingIP6ConfigPrivacy nm_l3_config_data_get_ip6_privacy(const NML3ConfigData
 gboolean nm_l3_config_data_set_ip6_privacy(NML3ConfigData           *self,
                                            NMSettingIP6ConfigPrivacy ip6_privacy);
 
+gboolean nm_l3_config_data_get_dhcp_enabled(const NML3ConfigData *self, int addr_family);
+
 NMProxyConfigMethod nm_l3_config_data_get_proxy_method(const NML3ConfigData *self);
 
 gboolean nm_l3_config_data_set_proxy_method(NML3ConfigData *self, NMProxyConfigMethod value);
diff --git a/src/core/nm-l3-ipv4ll.c b/src/core/nm-l3-ipv4ll.c
index ec871bef..551a90fe 100644
--- a/src/core/nm-l3-ipv4ll.c
+++ b/src/core/nm-l3-ipv4ll.c
@@ -390,23 +390,23 @@ _ipv4ll_addrgen(NML3IPv4LL *self, gboolean generate_new_addr)
     _ASSERT(self);
 
     /* MAC_HASH_KEY is the same as used by systemd. */
-#define MAC_HASH_KEY          \
-    ((const guint8[16]){0xdf, \
-                        0x04, \
-                        0x22, \
-                        0x98, \
-                        0x3f, \
-                        0xad, \
-                        0x14, \
-                        0x52, \
-                        0xf9, \
-                        0x87, \
-                        0x2e, \
-                        0xd1, \
-                        0x9c, \
-                        0x70, \
-                        0xe2, \
-                        0xf2})
+#define MAC_HASH_KEY      \
+    NM_HASH_SEED_16(0xdf, \
+                    0x04, \
+                    0x22, \
+                    0x98, \
+                    0x3f, \
+                    0xad, \
+                    0x14, \
+                    0x52, \
+                    0xf9, \
+                    0x87, \
+                    0x2e, \
+                    0xd1, \
+                    0x9c, \
+                    0x70, \
+                    0xe2, \
+                    0xf2)
 
     if (self->mac_set && (!self->seed_set || !nm_ether_addr_equal(&self->mac, &self->seed_mac))) {
         /* systemd's ipv4ll library by default only hashes the MAC address (as we do here).
@@ -465,23 +465,23 @@ _ipv4ll_addrgen(NML3IPv4LL *self, gboolean generate_new_addr)
 
 gen_addr:
 
-#define PICK_HASH_KEY         \
-    ((const guint8[16]){0x15, \
-                        0xac, \
-                        0x82, \
-                        0xa6, \
-                        0xd6, \
-                        0x3f, \
-                        0x49, \
-                        0x78, \
-                        0x98, \
-                        0x77, \
-                        0x5d, \
-                        0x0c, \
-                        0x69, \
-                        0x02, \
-                        0x94, \
-                        0x0b})
+#define PICK_HASH_KEY     \
+    NM_HASH_SEED_16(0x15, \
+                    0xac, \
+                    0x82, \
+                    0xa6, \
+                    0xd6, \
+                    0x3f, \
+                    0x49, \
+                    0x78, \
+                    0x98, \
+                    0x77, \
+                    0x5d, \
+                    0x0c, \
+                    0x69, \
+                    0x02, \
+                    0x94, \
+                    0x0b)
 
     h = c_siphash_hash(PICK_HASH_KEY, (const guint8 *) &self->seed, sizeof(self->seed));
 
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index ce9d5849..f428d04c 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -165,7 +165,18 @@ typedef struct {
 
     /* This flag is only used temporarily to do a bulk update and
      * clear all the ones that are no longer in used. */
-    bool os_dirty : 1;
+    bool os_non_dynamic_dirty : 1;
+
+    /* Indicates that we have a object in combined_l3cd_commited that keeps the
+     * object state alive. */
+    bool os_non_dynamic : 1;
+
+    /* Indicates that there is a dynamic route from _commit_collect_routes(), that keeps the
+      * object state alive. */
+    bool os_dynamic : 1;
+
+    /* Indicates that this dynamic obj-state is marked as dirty. */
+    bool os_dynamic_dirty : 1;
 } ObjStateData;
 
 G_STATIC_ASSERT(G_STRUCT_OFFSET(ObjStateData, obj) == 0);
@@ -334,6 +345,7 @@ typedef struct _NML3CfgPrivate {
 
     bool nacd_acd_not_supported : 1;
     bool acd_ipv4_addresses_on_link_has : 1;
+    bool acd_data_pruning_needed : 1;
 
     bool changed_configs_configs : 1;
     bool changed_configs_acd_state : 1;
@@ -394,7 +406,8 @@ static void _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is
 
 static void _nm_l3cfg_emit_signal_notify_acd_event_all(NML3Cfg *self);
 
-static gboolean _acd_has_valid_link(const NMPObject *obj,
+static gboolean _acd_has_valid_link(NML3Cfg         *self,
+                                    const NMPObject *obj,
                                     const guint8   **out_addr_bin,
                                     gboolean        *out_acd_not_supported);
 
@@ -674,9 +687,9 @@ _nm_l3cfg_emit_signal_notify_l3cd_changed(NML3Cfg              *self,
 /*****************************************************************************/
 
 static void
-_l3_changed_configs_set_dirty(NML3Cfg *self)
+_l3_changed_configs_set_dirty(NML3Cfg *self, const char *reason)
 {
-    _LOGT("IP configuration changed (mark dirty)");
+    _LOGT("IP configuration changed (mark dirty): %s", reason);
     self->priv.p->changed_configs_configs   = TRUE;
     self->priv.p->changed_configs_acd_state = TRUE;
 }
@@ -770,51 +783,70 @@ _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_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)));                                          \
-                    }                                                                            \
-                }                                                                                \
-            }                                                                                    \
-        }                                                                                        \
-    }                                                                                            \
+#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);          \
+            nm_assert(!_obj_state->os_plobj                                                   \
+                      || NMP_OBJECT_GET_TYPE(_obj_state->obj)                                 \
+                             == NMP_OBJECT_GET_TYPE(_obj_state->os_plobj));                   \
+            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) {                                                \
+                        /* metric-any must be resolved before adding the object. Otherwise,
+                         * their real metric is not known, and they cannot be compared to objects
+                         * from NMPlatform cache. */   \
+                        nm_assert(!NM_IN_SET(NMP_OBJECT_GET_TYPE(_obj_state->obj),            \
+                                             NMP_OBJECT_TYPE_IP4_ROUTE,                       \
+                                             NMP_OBJECT_TYPE_IP6_ROUTE)                       \
+                                  || !NMP_OBJECT_CAST_IP_ROUTE(_obj_state->obj)->metric_any); \
+                                                                                              \
+                        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));                \
+                        if (!c_list_is_empty(&obj_state->os_zombie_lst)) {                    \
+                            nm_assert(!obj_state->os_non_dynamic);                            \
+                            nm_assert(!obj_state->os_non_dynamic_dirty);                      \
+                            nm_assert(!obj_state->os_dynamic);                                \
+                            nm_assert(!obj_state->os_dynamic_dirty);                          \
+                        }                                                                     \
+                        if (obj_state->os_non_dynamic) {                                      \
+                            nm_assert(                                                        \
+                                _obj_state->obj                                               \
+                                == nm_dedup_multi_entry_get_obj(nm_l3_config_data_lookup_obj( \
+                                    _self->priv.p->combined_l3cd_commited,                    \
+                                    _obj_state->obj)));                                       \
+                        } else {                                                              \
+                            nm_assert(!nm_l3_config_data_lookup_obj(                          \
+                                _self->priv.p->combined_l3cd_commited,                        \
+                                _obj_state->obj));                                            \
+                        }                                                                     \
+                    }                                                                         \
+                }                                                                             \
+            }                                                                                 \
+        }                                                                                     \
+    }                                                                                         \
     G_STMT_END
 
 static ObjStateData *
@@ -828,7 +860,10 @@ _obj_state_data_new(const NMPObject *obj, const NMPObject *plobj)
         .os_plobj                 = nmp_object_ref(plobj),
         .os_was_in_platform       = !!plobj,
         .os_nm_configured         = FALSE,
-        .os_dirty                 = FALSE,
+        .os_non_dynamic_dirty     = FALSE,
+        .os_non_dynamic           = FALSE,
+        .os_dynamic               = FALSE,
+        .os_dynamic_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),
@@ -897,34 +932,6 @@ _obj_state_data_to_string(const ObjStateData *obj_state, char *buf, gsize buf_si
     return buf0;
 }
 
-static gboolean
-_obj_state_data_update(ObjStateData *obj_state, const NMPObject *obj)
-{
-    gboolean changed = FALSE;
-
-    nm_assert_obj_state(NULL, obj_state);
-    nm_assert(obj);
-    nm_assert(nmp_object_id_equal(obj_state->obj, obj));
-
-    obj_state->os_dirty = FALSE;
-
-    if (obj_state->obj != obj) {
-        nm_auto_nmpobj const NMPObject *obj_old = NULL;
-
-        if (!nmp_object_equal(obj_state->obj, obj))
-            changed = TRUE;
-        obj_old        = g_steal_pointer(&obj_state->obj);
-        obj_state->obj = nmp_object_ref(obj);
-    }
-
-    if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
-        c_list_unlink(&obj_state->os_zombie_lst);
-        changed = TRUE;
-    }
-
-    return changed;
-}
-
 /*****************************************************************************/
 
 static void
@@ -999,6 +1006,151 @@ out:
 }
 
 static void
+_obj_states_track_new(NML3Cfg *self, const NMPObject *obj, gboolean dynamic)
+{
+    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+    ObjStateData *obj_state;
+
+    obj_state = _obj_state_data_new(
+        obj,
+        nm_platform_lookup_obj(self->priv.platform, NMP_CACHE_ID_TYPE_OBJECT_TYPE, obj));
+    obj_state->os_dynamic     = dynamic;
+    obj_state->os_non_dynamic = !dynamic;
+    c_list_link_tail(&self->priv.p->obj_state_lst_head, &obj_state->os_lst);
+    g_hash_table_add(self->priv.p->obj_state_hash, obj_state);
+    _LOGD("obj-state: track%s: %s",
+          dynamic ? " (dynamic)" : "",
+          _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+    nm_assert_obj_state(self, obj_state);
+}
+
+static gboolean
+_obj_states_track_update(NML3Cfg         *self,
+                         ObjStateData    *obj_state,
+                         const NMPObject *obj,
+                         gboolean         dynamic)
+{
+    gboolean changed = FALSE;
+
+    nm_assert_obj_state(NULL, obj_state);
+    nm_assert(obj);
+    nm_assert(nmp_object_id_equal(obj_state->obj, obj));
+
+    if (dynamic) {
+        if (!obj_state->os_dynamic)
+            changed = TRUE;
+        obj_state->os_dynamic_dirty = FALSE;
+        obj_state->os_dynamic       = TRUE;
+    } else {
+        if (!obj_state->os_non_dynamic)
+            changed = TRUE;
+        obj_state->os_non_dynamic_dirty = FALSE;
+        obj_state->os_non_dynamic       = TRUE;
+    }
+
+    if (obj_state->obj != obj && (!dynamic || !obj_state->os_non_dynamic)) {
+        nm_auto_nmpobj const NMPObject *obj_old = NULL;
+
+        if (!nmp_object_equal(obj_state->obj, obj))
+            changed = TRUE;
+        obj_old        = g_steal_pointer(&obj_state->obj);
+        obj_state->obj = nmp_object_ref(obj);
+    }
+
+    if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
+        c_list_unlink(&obj_state->os_zombie_lst);
+        changed = TRUE;
+    }
+
+    if (changed) {
+        char sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+        _LOGD("obj-state: update: %s (static: %d, dynamic: %d)",
+              _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)),
+              !!obj_state->os_non_dynamic,
+              !!obj_state->os_dynamic);
+    }
+
+    nm_assert_obj_state(self, obj_state);
+    return changed;
+}
+
+static gboolean
+_obj_states_track_mark_dirty(NML3Cfg *self, gboolean dynamic)
+{
+    ObjStateData *obj_state;
+    gboolean      any_dirty = FALSE;
+
+    c_list_for_each_entry (obj_state, &self->priv.p->obj_state_lst_head, os_lst) {
+        if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
+            /* we can ignore zombies. */
+            continue;
+        }
+        if (dynamic) {
+            if (!obj_state->os_dynamic)
+                continue;
+            obj_state->os_dynamic_dirty = TRUE;
+        } else {
+            if (!obj_state->os_non_dynamic)
+                continue;
+            obj_state->os_non_dynamic_dirty = TRUE;
+        }
+        any_dirty = TRUE;
+    }
+
+    return any_dirty;
+}
+
+static void
+_obj_states_track_prune_dirty(NML3Cfg *self, gboolean also_dynamic)
+{
+    GHashTableIter h_iter;
+    ObjStateData  *obj_state;
+    char           sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
+
+    g_hash_table_iter_init(&h_iter, self->priv.p->obj_state_hash);
+    while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj_state, NULL)) {
+        if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
+            /* The object is half-dead already and only kept for cleanup. But
+             * it does not need to be untracked. */
+            continue;
+        }
+
+        /* Resolve the "dirty" flags. */
+        if (obj_state->os_non_dynamic_dirty) {
+            obj_state->os_non_dynamic       = FALSE;
+            obj_state->os_non_dynamic_dirty = FALSE;
+        }
+        if (also_dynamic) {
+            if (obj_state->os_dynamic_dirty) {
+                obj_state->os_dynamic       = FALSE;
+                obj_state->os_dynamic_dirty = FALSE;
+            }
+        }
+
+        if (obj_state->os_non_dynamic || obj_state->os_dynamic) {
+            /* This obj-state is still alive. Keep it. */
+            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;
+            _LOGD("obj-state: now zombie: %s",
+                  _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
+            continue;
+        }
+
+        _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);
+    }
+}
+
+static void
 _obj_states_update_all(NML3Cfg *self)
 {
     static const NMPObjectType obj_types[] = {
@@ -1007,21 +1159,13 @@ _obj_states_update_all(NML3Cfg *self)
         NMP_OBJECT_TYPE_IP4_ROUTE,
         NMP_OBJECT_TYPE_IP6_ROUTE,
     };
-    char          sbuf[NM_UTILS_TO_STRING_BUFFER_SIZE];
     ObjStateData *obj_state;
     int           i;
     gboolean      any_dirty = FALSE;
 
     nm_assert(NM_IS_L3CFG(self));
 
-    c_list_for_each_entry (obj_state, &self->priv.p->obj_state_lst_head, os_lst) {
-        if (!c_list_is_empty(&obj_state->os_zombie_lst)) {
-            /* we can ignore zombies. */
-            continue;
-        }
-        any_dirty           = TRUE;
-        obj_state->os_dirty = TRUE;
-    }
+    any_dirty = _obj_states_track_mark_dirty(self, FALSE);
 
     for (i = 0; i < (int) G_N_ELEMENTS(obj_types); i++) {
         const NMPObjectType obj_type = obj_types[i];
@@ -1039,59 +1183,24 @@ _obj_states_update_all(NML3Cfg *self)
                 /* this is a nodev route. We don't track an obj-state for this. */
                 continue;
             }
-
+            if (obj_type == NMP_OBJECT_TYPE_IP4_ROUTE
+                && NMP_OBJECT_CAST_IP4_ROUTE(obj)->weight > 0) {
+                /* this route weight is bigger than 0, that means we don't know
+                 * which kind of route this will be. It can only be determined during commit. */
+                continue;
+            }
             obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
             if (!obj_state) {
-                obj_state =
-                    _obj_state_data_new(obj,
-                                        nm_platform_lookup_obj(self->priv.platform,
-                                                               NMP_CACHE_ID_TYPE_OBJECT_TYPE,
-                                                               obj));
-                c_list_link_tail(&self->priv.p->obj_state_lst_head, &obj_state->os_lst);
-                g_hash_table_add(self->priv.p->obj_state_hash, obj_state);
-                _LOGD("obj-state: track: %s",
-                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
-                nm_assert_obj_state(self, obj_state);
+                _obj_states_track_new(self, obj, FALSE);
                 continue;
             }
 
-            if (_obj_state_data_update(obj_state, obj)) {
-                _LOGD("obj-state: update: %s",
-                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
-            }
-
-            nm_assert_obj_state(self, obj_state);
+            _obj_states_track_update(self, obj_state, obj, FALSE);
         }
     }
 
-    if (any_dirty) {
-        GHashTableIter h_iter;
-
-        g_hash_table_iter_init(&h_iter, self->priv.p->obj_state_hash);
-        while (g_hash_table_iter_next(&h_iter, (gpointer *) &obj_state, NULL)) {
-            if (!c_list_is_empty(&obj_state->os_zombie_lst))
-                continue;
-            if (!obj_state->os_dirty)
-                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;
-                _LOGD("obj-state: now zombie: %s",
-                      _obj_state_data_to_string(obj_state, sbuf, sizeof(sbuf)));
-                continue;
-            }
-
-            _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);
-        }
-    }
+    if (any_dirty)
+        _obj_states_track_prune_dirty(self, FALSE);
 }
 
 typedef struct {
@@ -1192,6 +1301,7 @@ _commit_collect_routes(NML3Cfg          *self,
     const int                    IS_IPv4 = NM_IS_IPv4(addr_family);
     const NMDedupMultiHeadEntry *head_entry;
     const NMDedupMultiEntry     *entry;
+    gboolean                     is_dhcp_enabled;
 
     nm_assert(routes && !*routes);
     nm_assert(routes_nodev && !*routes_nodev);
@@ -1211,17 +1321,21 @@ _commit_collect_routes(NML3Cfg          *self,
         else {
             nm_assert(NMP_OBJECT_CAST_IP_ROUTE(obj)->ifindex == self->priv.ifindex);
 
-            if (!any_addrs) {
+            is_dhcp_enabled =
+                nm_l3_config_data_get_dhcp_enabled(self->priv.p->combined_l3cd_commited,
+                                                   addr_family);
+            if (!any_addrs && is_dhcp_enabled) {
                 /* 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.
+                 * IP addresses when the profile has `ipvx.method=auto` and
+                 * static routes.
                  *
                  * Don't configure any such routes before we also have at least
-                 * one IP address.
+                 * one IP address except for `ipvx.method=manual` where static
+                 * routes are allowed to configure even if the connection has
+                 * no addresses
                  *
                  * This code applies to IPv4 and IPv6, however for IPv6 we
                  * early on configure a link local address, so in practice the
@@ -1263,6 +1377,13 @@ loop_done:
         if (singlehop_routes) {
             for (i = 0; i < singlehop_routes->len; i++) {
                 const NMPObject *obj = singlehop_routes->pdata[i];
+                ObjStateData    *obj_state;
+
+                obj_state = g_hash_table_lookup(self->priv.p->obj_state_hash, &obj);
+                if (!obj_state)
+                    _obj_states_track_new(self, obj, TRUE);
+                else
+                    _obj_states_track_update(self, obj_state, obj, TRUE);
 
                 if (!_obj_states_sync_filter(self, obj, commit_type))
                     continue;
@@ -1403,8 +1524,8 @@ _load_link(NML3Cfg *self, gboolean initial)
         nacd_link_now_up = FALSE;
 
     nacd_changed   = FALSE;
-    nacd_old_valid = _acd_has_valid_link(obj_old, &nacd_old_addr, NULL);
-    nacd_new_valid = _acd_has_valid_link(obj, &nacd_new_addr, NULL);
+    nacd_old_valid = _acd_has_valid_link(self, obj_old, &nacd_old_addr, NULL);
+    nacd_new_valid = _acd_has_valid_link(self, obj, &nacd_new_addr, NULL);
     if (self->priv.p->nacd_instance_ensure_retry) {
         if (nacd_new_valid
             && (!nacd_old_valid
@@ -1614,7 +1735,8 @@ _acd_data_find_track(const AcdData        *acd_data,
 /*****************************************************************************/
 
 static gboolean
-_acd_has_valid_link(const NMPObject *obj,
+_acd_has_valid_link(NML3Cfg         *self,
+                    const NMPObject *obj,
                     const guint8   **out_addr_bin,
                     gboolean        *out_acd_not_supported)
 {
@@ -1635,6 +1757,11 @@ _acd_has_valid_link(const NMPObject *obj,
         return FALSE;
     }
 
+    if (nm_platform_link_get_ifi_flags(self->priv.platform, self->priv.ifindex, IFF_NOARP)) {
+        NM_SET_OUT(out_acd_not_supported, TRUE);
+        return FALSE;
+    }
+
     NM_SET_OUT(out_acd_not_supported, FALSE);
     NM_SET_OUT(out_addr_bin, addr_bin);
     return TRUE;
@@ -1787,7 +1914,7 @@ _l3_acd_nacd_instance_ensure_retry_cb(gpointer user_data)
 
     nm_clear_g_source_inst(&self->priv.p->nacd_instance_ensure_retry);
 
-    _l3_changed_configs_set_dirty(self);
+    _l3_changed_configs_set_dirty(self, "nacd retry");
     nm_l3cfg_commit(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
     return G_SOURCE_REMOVE;
 }
@@ -1810,7 +1937,7 @@ _l3_acd_nacd_instance_reset(NML3Cfg *self, NMTernary start_timer, gboolean acd_d
 
     switch (start_timer) {
     case NM_TERNARY_FALSE:
-        _l3_changed_configs_set_dirty(self);
+        _l3_changed_configs_set_dirty(self, "nacd reset");
         nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
         break;
     case NM_TERNARY_TRUE:
@@ -1864,7 +1991,7 @@ again:
         return NULL;
     }
 
-    valid = _acd_has_valid_link(self->priv.plobj, &addr_bin, &acd_not_supported);
+    valid = _acd_has_valid_link(self, self->priv.plobj, &addr_bin, &acd_not_supported);
     if (!valid)
         goto failed_create_acd;
 
@@ -2325,7 +2452,7 @@ _nm_printf(5, 6) static void _l3_acd_data_state_set_full(NML3Cfg         *self,
     if (changed && allow_commit) {
         /* The availability of an address just changed (and we are instructed to
          * trigger a new commit). Do it. */
-        _l3_changed_configs_set_dirty(self);
+        _l3_changed_configs_set_dirty(self, "acd state changed");
         nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
     }
 }
@@ -2972,8 +3099,7 @@ handle_start_defending:
                                         NM_L3_ACD_ADDR_STATE_READY,
                                         !NM_IN_SET(state_change_mode,
                                                    ACD_STATE_CHANGE_MODE_INIT,
-                                                   ACD_STATE_CHANGE_MODE_INIT_REAPPLY,
-                                                   ACD_STATE_CHANGE_MODE_POST_COMMIT),
+                                                   ACD_STATE_CHANGE_MODE_INIT_REAPPLY),
                                         "probe is ready, waiting for address to be configured");
         }
         return;
@@ -3053,7 +3179,10 @@ _l3_acd_data_process_changes(NML3Cfg *self)
     AcdData *acd_data;
     gint64   now_msec = 0;
 
-    _l3_acd_data_prune(self, FALSE);
+    if (self->priv.p->acd_data_pruning_needed)
+        _l3_acd_data_prune(self, FALSE);
+
+    self->priv.p->acd_data_pruning_needed = FALSE;
 
     c_list_for_each_entry (acd_data, &self->priv.p->acd_lst_head, acd_lst) {
         _l3_acd_data_state_change(self,
@@ -3271,7 +3400,7 @@ nm_l3cfg_commit_on_idle_schedule(NML3Cfg *self, NML3CfgCommitType commit_type)
     if (self->priv.p->commit_on_idle_source) {
         if (self->priv.p->commit_on_idle_type < commit_type) {
             /* For multiple calls, we collect the maximum "commit-type". */
-            _LOGT("commit on idle (scheduled) (update to %s)",
+            _LOGT("schedule commit on idle (upgrade type to %s)",
                   _l3_cfg_commit_type_to_string(commit_type,
                                                 sbuf_commit_type,
                                                 sizeof(sbuf_commit_type)));
@@ -3280,7 +3409,7 @@ nm_l3cfg_commit_on_idle_schedule(NML3Cfg *self, NML3CfgCommitType commit_type)
         return FALSE;
     }
 
-    _LOGT("commit on idle (scheduled) (%s)",
+    _LOGT("schedule commit on idle (%s)",
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
     self->priv.p->commit_on_idle_source = nm_g_idle_add_source(_l3_commit_on_idle_cb, self);
     self->priv.p->commit_on_idle_type   = commit_type;
@@ -3553,7 +3682,7 @@ nm_l3cfg_add_config(NML3Cfg              *self,
     nm_assert(l3_config_data->acd_defend_type_confdata == acd_defend_type);
 
     if (changed) {
-        _l3_changed_configs_set_dirty(self);
+        _l3_changed_configs_set_dirty(self, "configuration added");
         nm_l3cfg_commit_on_idle_schedule(self, NM_L3_CFG_COMMIT_TYPE_AUTO);
     }
 
@@ -3590,7 +3719,7 @@ _l3cfg_remove_config(NML3Cfg              *self,
             continue;
         }
 
-        _l3_changed_configs_set_dirty(self);
+        _l3_changed_configs_set_dirty(self, "configuration removed");
         _l3_config_datas_remove_index_fast(self->priv.p->l3_config_datas, idx);
         changed = TRUE;
         if (l3cd) {
@@ -3783,6 +3912,9 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
 
     NM_SET_OUT(out_changed_combined_l3cd, FALSE);
 
+    if (to_commit)
+        self->priv.p->acd_data_pruning_needed = FALSE;
+
     if (!self->priv.p->changed_configs_configs) {
         if (!self->priv.p->changed_configs_acd_state)
             goto out;
@@ -3830,6 +3962,7 @@ _l3cfg_update_combined_config(NML3Cfg               *self,
         self->priv.p->changed_configs_acd_state = TRUE;
     } else {
         _l3_acd_data_add_all(self, l3_config_datas_arr, l3_config_datas_len, reapply);
+        self->priv.p->acd_data_pruning_needed   = TRUE;
         self->priv.p->changed_configs_acd_state = FALSE;
     }
 
@@ -4827,6 +4960,7 @@ _l3_commit_one(NML3Cfg              *self,
     NMIPRouteTableSyncMode       route_table_sync;
     char                         sbuf_commit_type[50];
     guint                        i;
+    gboolean                     any_dirty = FALSE;
 
     nm_assert(NM_IS_L3CFG(self));
     nm_assert(NM_IN_SET(commit_type,
@@ -4839,6 +4973,9 @@ _l3_commit_one(NML3Cfg              *self,
           nm_utils_addr_family_to_char(addr_family),
           _l3_cfg_commit_type_to_string(commit_type, sbuf_commit_type, sizeof(sbuf_commit_type)));
 
+    if (IS_IPv4)
+        any_dirty = _obj_states_track_mark_dirty(self, TRUE);
+
     addresses = _commit_collect_addresses(self, addr_family, commit_type);
 
     _commit_collect_routes(self,
@@ -4863,6 +5000,9 @@ _l3_commit_one(NML3Cfg              *self,
     if (route_table_sync == NM_IP_ROUTE_TABLE_SYNC_MODE_NONE)
         route_table_sync = NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN;
 
+    if (any_dirty)
+        _obj_states_track_prune_dirty(self, TRUE);
+
     if (commit_type == NM_L3_CFG_COMMIT_TYPE_REAPPLY) {
         gs_unref_array GArray *ipv6_temp_addrs_keep = NULL;
 
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index 9b8ec67f..241fde02 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -34,8 +34,8 @@ typedef enum _nm_packed {
 /**
  * NML3CfgConfigFlags:
  * @NM_L3CFG_CONFIG_FLAGS_NONE: no flags, the default.
- * @NM_L3_CONFIG_MERGE_FLAGS_ONLY_FOR_ACD: if this merge flag is set,
- *   the the NML3ConfigData doesn't get merged and it's information won't be
+ * @NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD: if this flag is set,
+ *   the NML3ConfigData doesn't get merged and the information won't be
  *   synced. The only purpose is to run ACD on its IPv4 addresses, but
  *   regardless whether ACD succeeds/fails, the IP addresses won't be configured.
  *   The point is to run ACD first (without configuring it), and only
diff --git a/src/core/nm-manager.c b/src/core/nm-manager.c
index 937acbba..9c58a47d 100644
--- a/src/core/nm-manager.c
+++ b/src/core/nm-manager.c
@@ -66,7 +66,6 @@ typedef struct {
     bool user_enabled : 1;
     bool sw_enabled : 1;
     bool hw_enabled : 1;
-    bool os_owner : 1;
 } RfkillRadioState;
 
 #define AUTOCONNECT_RESET_RETRIES_TIMER_SEC 300
@@ -800,7 +799,7 @@ initited:
          * a lot of stale entries. We must from time to time clean them up.
          *
          * Do do this cleanup, whenever we have more entries then 2 times the number of links. */
-        if (G_UNLIKELY(g_hash_table_size(priv->device_route_metrics) > NM_MAX(20, n_links * 2))) {
+        if (G_UNLIKELY(g_hash_table_size(priv->device_route_metrics) > NM_MAX(20u, n_links * 2u))) {
             /* from time to time, we need to do some house-keeping and prune stale entries.
              * Otherwise, on a system where interfaces frequently come and go (docker), we
              * keep growing this cache for ifindexes that no longer exist. */
@@ -2893,9 +2892,6 @@ _rfkill_radio_state_get_enabled(const RfkillRadioState *rstate, gboolean check_c
 {
     gboolean enabled;
 
-    /* If the device is not owned by the os, hw_enabled will be FALSE, hence
-     * we don't need to consider os_owner here.
-     */
     enabled = rstate->user_enabled && rstate->hw_enabled;
     if (check_changeable)
         enabled &= rstate->sw_enabled;
@@ -2912,7 +2908,6 @@ _rfkill_radio_state_set_from_manager(NMManager *self, NMRfkillType rtype, Rfkill
     case NM_RFKILL_STATE_UNAVAILABLE:
         rstate->sw_enabled = TRUE;
         rstate->hw_enabled = TRUE;
-        rstate->os_owner   = TRUE;
 
         /* A rfkill-type is available when there is a compatible
          * killswitch or a compatible device. */
@@ -2928,26 +2923,16 @@ _rfkill_radio_state_set_from_manager(NMManager *self, NMRfkillType rtype, Rfkill
         rstate->available  = TRUE;
         rstate->sw_enabled = TRUE;
         rstate->hw_enabled = TRUE;
-        rstate->os_owner   = TRUE;
         return;
     case NM_RFKILL_STATE_SOFT_BLOCKED:
         rstate->available  = TRUE;
         rstate->sw_enabled = FALSE;
         rstate->hw_enabled = TRUE;
-        rstate->os_owner   = TRUE;
         return;
     case NM_RFKILL_STATE_HARD_BLOCKED:
         rstate->available  = TRUE;
         rstate->sw_enabled = FALSE;
         rstate->hw_enabled = FALSE;
-        /* In case the OS doesn't own the NIC, we would be in NM_RFKILL_STATE_HARD_BLOCKED */
-        rstate->os_owner = TRUE;
-        return;
-    case NM_RFKILL_STATE_HARD_BLOCKED_OS_NOT_OWNER:
-        rstate->available  = TRUE;
-        rstate->sw_enabled = FALSE;
-        rstate->hw_enabled = FALSE;
-        rstate->os_owner   = FALSE;
         return;
     }
     nm_assert_not_reached();
@@ -3011,12 +2996,11 @@ _rfkill_update_one_type(NMManager *self, NMRfkillType rtype)
 
     /* Print out all states affecting device enablement */
     _LOGD(LOGD_RFKILL,
-          "rfkill: %s available %d hw-enabled %d sw-enabled %d os-owner %d",
+          "rfkill: %s available %d hw-enabled %d sw-enabled %d",
           nm_rfkill_type_to_string(rtype),
           rstate->available,
           rstate->hw_enabled,
-          rstate->sw_enabled,
-          rstate->os_owner);
+          rstate->sw_enabled);
 
     /* Log new killswitch state */
     new_rfkilled = rstate->hw_enabled && rstate->sw_enabled;
@@ -4440,11 +4424,7 @@ platform_query_devices(NMManager *self)
     gs_free char                *order = NULL;
 
     guess_assume = nm_config_get_first_start(nm_config_get());
-    order        = nm_config_data_get_value(NM_CONFIG_GET_DATA,
-                                     NM_CONFIG_KEYFILE_GROUP_MAIN,
-                                     NM_CONFIG_KEYFILE_KEY_MAIN_SLAVES_ORDER,
-                                     NM_CONFIG_GET_VALUE_STRIP);
-    links        = nm_platform_link_get_all(priv->platform, !nm_streq0(order, "index"));
+    links        = nm_platform_link_get_all(priv->platform);
     if (!links)
         return;
     for (i = 0; i < links->len; i++) {
@@ -4867,7 +4847,7 @@ is_compatible_with_slave(NMConnection *master, NMConnection *slave)
     s_con = nm_connection_get_setting_connection(slave);
     g_assert(s_con);
 
-    return nm_connection_is_type(master, nm_setting_connection_get_slave_type(s_con));
+    return nm_connection_is_type(master, nm_setting_connection_get_port_type(s_con));
 }
 
 /**
@@ -5380,7 +5360,7 @@ out:
 }
 
 static int
-compare_slaves(gconstpointer a, gconstpointer b, gpointer sort_by_name)
+compare_slaves(gconstpointer a, gconstpointer b)
 {
     const SlaveConnectionInfo *a_info = a;
     const SlaveConnectionInfo *b_info = b;
@@ -5391,11 +5371,7 @@ compare_slaves(gconstpointer a, gconstpointer b, gpointer sort_by_name)
     if (!b_info->device)
         return -1;
 
-    if (GPOINTER_TO_INT(sort_by_name)) {
-        return nm_strcmp0(nm_device_get_iface(a_info->device), nm_device_get_iface(b_info->device));
-    }
-
-    return nm_device_get_ifindex(a_info->device) - nm_device_get_ifindex(b_info->device);
+    return nm_strcmp0(nm_device_get_iface(a_info->device), nm_device_get_iface(b_info->device));
 }
 
 static void
@@ -5415,17 +5391,7 @@ autoconnect_slaves(NMManager            *self,
 
         slaves = find_slaves(self, master_connection, master_device, &n_slaves, for_user_request);
         if (n_slaves > 1) {
-            gs_free char *value = NULL;
-
-            value = nm_config_data_get_value(NM_CONFIG_GET_DATA,
-                                             NM_CONFIG_KEYFILE_GROUP_MAIN,
-                                             NM_CONFIG_KEYFILE_KEY_MAIN_SLAVES_ORDER,
-                                             NM_CONFIG_GET_VALUE_STRIP);
-            g_qsort_with_data(slaves,
-                              n_slaves,
-                              sizeof(slaves[0]),
-                              compare_slaves,
-                              GINT_TO_POINTER(!nm_streq0(value, "index")));
+            qsort(slaves, n_slaves, sizeof(slaves[0]), compare_slaves);
         }
 
         bind_lifetime_to_profile_visibility =
@@ -7354,14 +7320,12 @@ do_sleep_wake(NMManager *self, gboolean sleeping_changed)
                 gboolean                enabled = _rfkill_radio_state_get_enabled(rstate, TRUE);
 
                 _LOGD(LOGD_RFKILL,
-                      "rfkill: %s %s devices (hw_enabled %d, sw_enabled %d, user_enabled %d, "
-                      "os_owner %d)",
+                      "rfkill: %s %s devices (hw_enabled %d, sw_enabled %d, user_enabled %d)",
                       enabled ? "enabling" : "disabling",
                       nm_rfkill_type_to_string(rtype),
                       rstate->hw_enabled,
                       rstate->sw_enabled,
-                      rstate->user_enabled,
-                      rstate->os_owner);
+                      rstate->user_enabled);
                 if (nm_device_get_rfkill_type(device) == rtype)
                     nm_device_set_enabled(device, enabled);
             }
@@ -8846,13 +8810,11 @@ nm_manager_init(NMManager *self)
         .user_enabled = TRUE,
         .sw_enabled   = FALSE,
         .hw_enabled   = TRUE,
-        .os_owner     = TRUE,
     };
     priv->radio_states[NM_RFKILL_TYPE_WWAN] = (RfkillRadioState){
         .user_enabled = TRUE,
         .sw_enabled   = FALSE,
         .hw_enabled   = TRUE,
-        .os_owner     = TRUE,
     };
 
     priv->sleeping = FALSE;
@@ -9402,10 +9364,13 @@ static const NMDBusInterfaceInfoExtended interface_info_manager = {
                 "b",
                 NM_MANAGER_WIMAX_ENABLED,
                 NM_AUTH_PERMISSION_ENABLE_DISABLE_WIMAX,
-                NM_AUDIT_OP_RADIO_CONTROL),
-            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("WimaxHardwareEnabled",
-                                                           "b",
-                                                           NM_MANAGER_WIMAX_HARDWARE_ENABLED),
+                NM_AUDIT_OP_RADIO_CONTROL,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
+            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE(
+                "WimaxHardwareEnabled",
+                "b",
+                NM_MANAGER_WIMAX_HARDWARE_ENABLED,
+                .annotations = NM_GDBUS_ANNOTATION_INFO_LIST_DEPRECATED(), ),
             NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE("RadioFlags",
                                                            "u",
                                                            NM_MANAGER_RADIO_FLAGS),
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index ad156f99..ed33d336 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -905,11 +905,19 @@ nm_netns_ip_route_ecmp_commit(NMNetns    *self,
         if (obj_del) {
             if (NMP_OBJECT_CAST_IP4_ROUTE(obj_del)->n_nexthops > 1)
                 nm_platform_object_delete(priv->platform, obj_del);
-            else if (track_obj->l3cfg != l3cfg)
-                nm_l3cfg_commit_on_idle_schedule(track_obj->l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+            else if (NMP_OBJECT_CAST_IP4_ROUTE(obj_del)->ifindex != nm_l3cfg_get_ifindex(l3cfg)) {
+                /* A single-hop route from a different interface was merged
+                 * into a ECMP route. Now, it is time to notify the l3cfg that
+                 * is managing that single-hop route to remove it. */
+                nm_l3cfg_commit_on_idle_schedule(
+                    nm_netns_l3cfg_get(self, NMP_OBJECT_CAST_IP4_ROUTE(obj_del)->ifindex),
+                    NM_L3_CFG_COMMIT_TYPE_UPDATE);
+            }
         }
 
         if (route->n_nexthops <= 1) {
+            NMPObject *route_clone;
+
             /* This is a single hop route. Return it to the caller. */
             if (!*out_singlehop_routes) {
                 /* Note that the returned array does not own a reference. This
@@ -918,7 +926,28 @@ nm_netns_ip_route_ecmp_commit(NMNetns    *self,
                 *out_singlehop_routes =
                     g_ptr_array_new_with_free_func((GDestroyNotify) nmp_object_unref);
             }
-            g_ptr_array_add(*out_singlehop_routes, (gpointer) nmp_object_ref(route_obj));
+
+            /* We have here a IPv4 single-hop route. For internal tracking purposes,
+             * this route has a positive "weight" (which was used to mark it as a candidate
+             * for ECMP merging). Now we want to return this route to NML3Cfg and add it
+             * as regular single-hop routes.
+             *
+             * A single-hop route in kernel always has a "weight" of zero. This route
+             * cannot be added as-is. Well, if we would, then the result would be
+             * a different(!) route (with a zero "weight").
+             *
+             * Anticipate that and normalize the route now to be a regular single-hop
+             * route (with weight zero). nm_platform_ip_route_normalize() does that.
+             * We really want to return a regular route here, not the route with a positive
+             * weight that exists for internal tracking purposes.
+             */
+            nm_assert(NMP_OBJECT_GET_TYPE(route_obj) == NMP_OBJECT_TYPE_IP4_ROUTE);
+            nm_assert(route_obj->ip4_route.weight > 0u);
+
+            route_clone = nmp_object_clone(route_obj, FALSE);
+            nm_platform_ip_route_normalize(AF_INET, NMP_OBJECT_CAST_IP_ROUTE(route_clone));
+            g_ptr_array_add(*out_singlehop_routes, route_clone);
+
             if (changed) {
                 _LOGT("ecmp-route: single-hop %s",
                       nmp_object_to_string(route_obj,
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index efdb0636..feea97b1 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -1766,7 +1766,7 @@ _connection_autoconnect_retries_set(NMPolicy             *self,
             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()),
+                NM_MAX(0, retry_time - nm_utils_get_monotonic_timestamp_sec()),
                 reset_connections_retries,
                 self);
         }
@@ -2635,11 +2635,15 @@ dns_config_changed(NMDnsManager *dns_manager, gpointer user_data)
     if (priv->updating_dns)
         return;
 
-    nm_manager_for_each_device (priv->manager, device, tmp_lst) {
-        nm_device_clear_dns_lookup_data(device, "DNS configuration changed");
+    if (!nm_dns_manager_is_unmanaged(dns_manager)) {
+        nm_manager_for_each_device (priv->manager, device, tmp_lst) {
+            nm_device_clear_dns_lookup_data(device, "DNS configuration changed");
+        }
+
+        update_system_hostname(self, "DNS configuration changed");
     }
 
-    update_system_hostname(self, "DNS configuration changed");
+    nm_dispatcher_call_dns_change();
 }
 
 static void
diff --git a/src/core/nm-rfkill-manager.c b/src/core/nm-rfkill-manager.c
index 03333899..aea5df11 100644
--- a/src/core/nm-rfkill-manager.c
+++ b/src/core/nm-rfkill-manager.c
@@ -106,8 +106,6 @@ nm_rfkill_state_to_string(NMRfkillState state)
         return "soft-blocked";
     case NM_RFKILL_STATE_HARD_BLOCKED:
         return "hard-blocked";
-    case NM_RFKILL_STATE_HARD_BLOCKED_OS_NOT_OWNER:
-        return "hard-blocked-os-not-owner";
     }
     return nm_assert_unreachable_val("unknown");
 }
@@ -171,7 +169,7 @@ killswitch_destroy(Killswitch *ks)
 }
 
 static NMRfkillState
-sysfs_state_to_nm_state(int sysfs_state, int sysfs_reason)
+sysfs_state_to_nm_state(int sysfs_state)
 {
     switch (sysfs_state) {
     case 0:
@@ -179,11 +177,6 @@ sysfs_state_to_nm_state(int sysfs_state, int sysfs_reason)
     case 1:
         return NM_RFKILL_STATE_UNBLOCKED;
     case 2:
-        /* sysfs reason is a bitmap, in case we have both reasons (SIGNAL and NOT_OWNER), we want
-         * to consider the device as not owned.
-         */
-        if (sysfs_reason & 2)
-            return NM_RFKILL_STATE_HARD_BLOCKED_OS_NOT_OWNER;
         return NM_RFKILL_STATE_HARD_BLOCKED;
     default:
         nm_log_warn(LOGD_RFKILL, "unhandled rfkill state %d", sysfs_state);
@@ -214,7 +207,6 @@ recheck_killswitches(NMRfkillManager *self)
         struct udev_device *device;
         NMRfkillState       dev_state;
         int                 sysfs_state;
-        int                 sysfs_reason;
 
         device = udev_device_new_from_subsystem_sysname(nm_udev_client_get_udev(priv->udev_client),
                                                         "rfkill",
@@ -227,24 +219,15 @@ recheck_killswitches(NMRfkillManager *self)
                                          G_MININT,
                                          G_MAXINT,
                                          -1);
-
-        sysfs_reason = _nm_utils_ascii_str_to_int64(
-            udev_device_get_property_value(device, "RFKILL_HW_BLOCK_REASON"),
-            16,
-            G_MININT,
-            G_MAXINT,
-            1); /* defaults to SIGNAL in case the kernel does not support this */
-
-        dev_state = sysfs_state_to_nm_state(sysfs_state, sysfs_reason);
+        dev_state = sysfs_state_to_nm_state(sysfs_state);
 
         nm_log_dbg(LOGD_RFKILL,
-                   "%s rfkill%s switch %s state now %d/%s reason: 0x%x",
+                   "%s rfkill%s switch %s state now %d/%s",
                    nm_rfkill_type_to_string(ks->rtype),
                    ks->platform ? " platform" : "",
                    ks->name,
                    sysfs_state,
-                   nm_rfkill_state_to_string(dev_state),
-                   sysfs_reason);
+                   nm_rfkill_state_to_string(dev_state));
 
         if (ks->platform == FALSE) {
             if (dev_state > poll_states[ks->rtype])
diff --git a/src/core/nm-rfkill-manager.h b/src/core/nm-rfkill-manager.h
index 818ab4b2..7e90c9ac 100644
--- a/src/core/nm-rfkill-manager.h
+++ b/src/core/nm-rfkill-manager.h
@@ -12,13 +12,6 @@ typedef enum {
     NM_RFKILL_STATE_UNBLOCKED    = 1,
     NM_RFKILL_STATE_SOFT_BLOCKED = 2,
     NM_RFKILL_STATE_HARD_BLOCKED = 3,
-    /* NM_RFKILL_STATE_HARD_BLOCKED_OS_NOT_OWNER means that the CSME firmware
-     * is currently controlling the device. This feature is implmented on Intel
-     * wifi devices only.
-     * The NetworkManager can get ownership on the device, but it requires to
-     * first ask ownership through the iwlmei kernel module.
-     */
-    NM_RFKILL_STATE_HARD_BLOCKED_OS_NOT_OWNER = 4,
 } NMRfkillState;
 
 typedef enum {
diff --git a/src/core/nm-types.h b/src/core/nm-types.h
index 18f44ca9..104a2f4b 100644
--- a/src/core/nm-types.h
+++ b/src/core/nm-types.h
@@ -6,7 +6,7 @@
 #ifndef __NETWORKMANAGER_TYPES_H__
 #define __NETWORKMANAGER_TYPES_H__
 
-#if !((NETWORKMANAGER_COMPILATION) &NM_NETWORKMANAGER_COMPILATION_WITH_DAEMON)
+#if !((NETWORKMANAGER_COMPILATION) & NM_NETWORKMANAGER_COMPILATION_WITH_DAEMON)
 #error Cannot use this header.
 #endif
 
diff --git a/src/core/platform/nm-fake-platform.c b/src/core/platform/nm-fake-platform.c
index f6f377d4..74bbe4fb 100644
--- a/src/core/platform/nm-fake-platform.c
+++ b/src/core/platform/nm-fake-platform.c
@@ -720,6 +720,14 @@ link_change(NMPlatform                   *platform,
         obj_tmp->link.port_data.bond.prio     = port_data->bond.prio;
         link_set_obj(platform, device, obj_tmp);
         return TRUE;
+    case NM_PORT_KIND_BRIDGE:
+        obj_tmp                                  = nmp_object_clone(device->obj, FALSE);
+        obj_tmp->link.port_kind                  = NM_PORT_KIND_BRIDGE;
+        obj_tmp->link.port_data.bridge.path_cost = port_data->bridge.path_cost;
+        obj_tmp->link.port_data.bridge.priority  = port_data->bridge.priority;
+        obj_tmp->link.port_data.bridge.hairpin   = port_data->bridge.hairpin;
+        link_set_obj(platform, device, obj_tmp);
+        return TRUE;
     case NM_PORT_KIND_NONE:
         return TRUE;
     }
diff --git a/src/core/platform/tests/test-common.h b/src/core/platform/tests/test-common.h
index 2802d8fd..12d6e7ce 100644
--- a/src/core/platform/tests/test-common.h
+++ b/src/core/platform/tests/test-common.h
@@ -32,7 +32,7 @@
          * Normalize the requested_value to the kernel_value, if it look as if a rounding
          * error happens. If the difference is larger than +/- 1, no normalization happens! */   \
                                                                                                  \
-        ((_requested_value >= (NM_MAX(_kernel_value, 1) - 1))                                    \
+        ((_requested_value >= (NM_MAX(_kernel_value, (typeof(_kernel_value)) 1) - 1))            \
          && (_requested_value <= (NM_MIN(_kernel_value, ~((typeof(_kernel_value)) 0) - 1) + 1))) \
             ? _kernel_value                                                                      \
             : _requested_value;                                                                  \
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index 8a54ac48..c931605b 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -230,7 +230,6 @@ test_port(int controller, int port_type, SignalData *controller_changed)
                                                link_callback,
                                                SLAVE_NAME);
     SignalData *link_changed, *link_removed;
-    char       *value;
     NMLinkType  controller_type = nm_platform_link_get_type(NM_PLATFORM_GET, controller);
     gboolean    test_link_changed_signal_arg1;
     gboolean    test_link_changed_signal_arg2;
@@ -296,7 +295,7 @@ test_port(int controller, int port_type, SignalData *controller_changed)
             .prio     = prio_has ? 6 : 0,
         };
 
-        g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex_port, NULL, &bond_port, 0));
+        g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex_port, NULL, &bond_port, NULL, 0));
         accept_signals(link_changed, 1, 3);
 
         link = nmtstp_link_get(NM_PLATFORM_GET, ifindex_port, SLAVE_NAME);
@@ -306,13 +305,35 @@ test_port(int controller, int port_type, SignalData *controller_changed)
     } 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);
+            NMPlatformLinkBridgePort   bridge_port;
+            const NMPlatformLink      *link;
+            const NMPlatformLnkBridge *lnk;
+
+            link = nmtstp_link_get_typed(NM_PLATFORM_GET, 0, SLAVE_NAME, NM_LINK_TYPE_DUMMY);
+            g_assert(link);
+
+            lnk = nm_platform_link_get_lnk_bridge(NM_PLATFORM_GET, controller, NULL);
+            g_assert(lnk);
+
+            bridge_port = (NMPlatformLinkBridgePort){
+                .path_cost = 100,
+                .priority  = 614,
+                .hairpin   = 0,
+            };
+
+            g_assert(nm_platform_link_change(NM_PLATFORM_GET,
+                                             ifindex_port,
+                                             NULL,
+                                             NULL,
+                                             &bridge_port,
+                                             0));
+            accept_signals(link_changed, 1, 3);
+
+            link = nmtstp_link_get(NM_PLATFORM_GET, ifindex_port, SLAVE_NAME);
+            g_assert(link);
+            g_assert_cmpint(link->port_data.bridge.path_cost, ==, 100);
+            g_assert_cmpint(link->port_data.bridge.priority, ==, 614);
+            g_assert_cmpint(link->port_data.bridge.hairpin, ==, 0);
         }
     }
 
@@ -2692,7 +2713,7 @@ test_link_set_properties(void)
             | 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));
+    g_assert(nm_platform_link_change(NM_PLATFORM_GET, ifindex, &props, NULL, NULL, flags));
 
     link = nmtstp_link_get(NM_PLATFORM_GET, ifindex, "dummy1");
     g_assert(link);
diff --git a/src/core/platform/tests/test-platform-general.c b/src/core/platform/tests/test-platform-general.c
index cebd44f6..fd6aae82 100644
--- a/src/core/platform/tests/test-platform-general.c
+++ b/src/core/platform/tests/test-platform-general.c
@@ -44,7 +44,7 @@ test_link_get_all(void)
 
     platform = nm_linux_platform_new(NULL, TRUE, NM_PLATFORM_NETNS_SUPPORT_DEFAULT, TRUE);
 
-    links = nm_platform_link_get_all(platform, TRUE);
+    links = nm_platform_link_get_all(platform);
 }
 
 /*****************************************************************************/
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index 8796de36..7fc20298 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -3530,7 +3530,7 @@ load_plugins(NMSettings *self, const char *const *plugins, GError **error)
             continue;
         }
 
-        if (nm_strv_find_first(plugins, iter - plugins, pname) >= 0) {
+        if (nm_strv_contains(plugins, iter - plugins, pname)) {
             /* the plugin is already mentioned in the list previously.
              * Don't load a duplicate. */
             continue;
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 84a9479d..04e79725 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
@@ -306,7 +306,7 @@ check_if_bond_slave(shvarFile *ifcfg, NMSettingConnection *s_con)
         }
 
         g_object_set(s_con,
-                     NM_SETTING_CONNECTION_MASTER,
+                     NM_SETTING_CONNECTION_CONTROLLER,
                      v,
                      NM_SETTING_CONNECTION_SLAVE_TYPE,
                      NM_SETTING_BOND_SETTING_NAME,
@@ -340,7 +340,7 @@ check_if_team_slave(shvarFile *ifcfg, NMSettingConnection *s_con)
     }
 
     g_object_set(s_con,
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  v,
                  NM_SETTING_CONNECTION_SLAVE_TYPE,
                  NM_SETTING_TEAM_SETTING_NAME,
@@ -512,7 +512,7 @@ make_connection_setting(const char *file,
                           old_value,
                           v);
         } else {
-            g_object_set(s_con, NM_SETTING_CONNECTION_MASTER, v, NULL);
+            g_object_set(s_con, NM_SETTING_CONNECTION_CONTROLLER, v, NULL);
             g_object_set(s_con,
                          NM_SETTING_CONNECTION_SLAVE_TYPE,
                          NM_SETTING_BRIDGE_SETTING_NAME,
@@ -535,7 +535,7 @@ make_connection_setting(const char *file,
                           old_value,
                           v);
         } else {
-            g_object_set(s_con, NM_SETTING_CONNECTION_MASTER, v, NULL);
+            g_object_set(s_con, NM_SETTING_CONNECTION_CONTROLLER, v, NULL);
             g_object_set(s_con,
                          NM_SETTING_CONNECTION_SLAVE_TYPE,
                          NM_SETTING_OVS_PORT_SETTING_NAME,
@@ -555,7 +555,7 @@ make_connection_setting(const char *file,
                           old_value,
                           v);
         } else {
-            g_object_set(s_con, NM_SETTING_CONNECTION_MASTER, v, NULL);
+            g_object_set(s_con, NM_SETTING_CONNECTION_CONTROLLER, v, NULL);
             g_object_set(s_con,
                          NM_SETTING_CONNECTION_SLAVE_TYPE,
                          NM_SETTING_VRF_SETTING_NAME,
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 08deaf5a..e703bb73 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
@@ -1346,6 +1346,7 @@ write_ethtool_setting(NMConnection *connection, shvarFile *ifcfg, GError **error
         guint32              u32;
         gboolean             b;
         gboolean             any_option = FALSE;
+        char                 prop_name[300];
 
         s_con = nm_connection_get_setting_connection(connection);
         if (s_con) {
@@ -1426,6 +1427,30 @@ write_ethtool_setting(NMConnection *connection, shvarFile *ifcfg, GError **error
             any_option = TRUE;
         }
 
+        is_first = TRUE;
+        for (ethtool_id = _NM_ETHTOOL_ID_CHANNELS_FIRST; ethtool_id <= _NM_ETHTOOL_ID_CHANNELS_LAST;
+             ethtool_id++) {
+            if (nm_setting_option_get_uint32(NM_SETTING(s_ethtool),
+                                             nm_ethtool_data[ethtool_id]->optname,
+                                             &u32)) {
+                nm_sprintf_buf(prop_name, "ethtool.%s", nm_ethtool_data[ethtool_id]->optname);
+                set_error_unsupported(error, connection, prop_name, FALSE);
+                return FALSE;
+            }
+        }
+
+        is_first = TRUE;
+        for (ethtool_id = _NM_ETHTOOL_ID_EEE_FIRST; ethtool_id <= _NM_ETHTOOL_ID_EEE_LAST;
+             ethtool_id++) {
+            if (nm_setting_option_get_boolean(NM_SETTING(s_ethtool),
+                                              nm_ethtool_data[ethtool_id]->optname,
+                                              &b)) {
+                nm_sprintf_buf(prop_name, "ethtool.%s", nm_ethtool_data[ethtool_id]->optname);
+                set_error_unsupported(error, connection, prop_name, FALSE);
+                return FALSE;
+            }
+        }
+
         if (!any_option) {
             /* Write an empty dummy "-A" option without arguments. This is to
              * ensure that the reader will create an (all default) NMSettingEthtool.
@@ -2235,34 +2260,38 @@ write_connection_setting(NMSettingConnection *s_con, shvarFile *ifcfg, const cha
             master       = NULL;
         }
 
-        if (nm_setting_connection_is_slave_type(s_con, NM_SETTING_BOND_SETTING_NAME)) {
+        if (nm_streq0(nm_setting_connection_get_port_type(s_con), NM_SETTING_BOND_SETTING_NAME)) {
             svSetValueStr(ifcfg, "MASTER_UUID", master);
             svSetValueStr(ifcfg, "MASTER", master_iface);
             svSetValueStr(ifcfg, "SLAVE", "yes");
-        } else if (nm_setting_connection_is_slave_type(s_con, NM_SETTING_BRIDGE_SETTING_NAME)) {
+        } else if (nm_streq0(nm_setting_connection_get_port_type(s_con),
+                             NM_SETTING_BRIDGE_SETTING_NAME)) {
             svSetValueStr(ifcfg, "BRIDGE_UUID", master);
             svSetValueStr(ifcfg, "BRIDGE", master_iface);
-        } else if (nm_setting_connection_is_slave_type(s_con, NM_SETTING_TEAM_SETTING_NAME)) {
+        } else if (nm_streq0(nm_setting_connection_get_port_type(s_con),
+                             NM_SETTING_TEAM_SETTING_NAME)) {
             svSetValueStr(ifcfg, "TEAM_MASTER_UUID", master);
             svSetValueStr(ifcfg, "TEAM_MASTER", master_iface);
             if (NM_IN_STRSET(type, NM_SETTING_WIRED_SETTING_NAME, NM_SETTING_VLAN_SETTING_NAME))
                 svUnsetValue(ifcfg, "TYPE");
-        } else if (nm_setting_connection_is_slave_type(s_con, NM_SETTING_OVS_PORT_SETTING_NAME)) {
+        } else if (nm_streq0(nm_setting_connection_get_port_type(s_con),
+                             NM_SETTING_OVS_PORT_SETTING_NAME)) {
             svSetValueStr(ifcfg, "OVS_PORT_UUID", master);
             svSetValueStr(ifcfg, "OVS_PORT", master_iface);
-        } else if (nm_setting_connection_is_slave_type(s_con, NM_SETTING_VRF_SETTING_NAME)) {
+        } else if (nm_streq0(nm_setting_connection_get_port_type(s_con),
+                             NM_SETTING_VRF_SETTING_NAME)) {
             svSetValueStr(ifcfg, "VRF_UUID", master);
             svSetValueStr(ifcfg, "VRF", master_iface);
         } else {
             _LOGW("don't know how to set master for a %s slave",
-                  nm_setting_connection_get_slave_type(s_con));
+                  nm_setting_connection_get_port_type(s_con));
         }
     }
 
     if (nm_streq0(type, NM_SETTING_TEAM_SETTING_NAME))
         svSetValueStr(ifcfg, "DEVICETYPE", TYPE_TEAM);
     else if (master_iface
-             && nm_setting_connection_is_slave_type(s_con, NM_SETTING_TEAM_SETTING_NAME))
+             && nm_streq0(nm_setting_connection_get_port_type(s_con), NM_SETTING_TEAM_SETTING_NAME))
         svSetValueStr(ifcfg, "DEVICETYPE", TYPE_TEAM_PORT);
 
     /* secondary connection UUIDs */
diff --git a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
index 8c1e9d94..1e6b6c25 100644
--- a/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
+++ b/src/core/settings/plugins/ifcfg-rh/tests/test-ifcfg-rh.c
@@ -2231,7 +2231,7 @@ test_clear_master(void)
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
 
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con), ==, "bridge");
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, "bridge");
 
     /* 2. write the connection to a new file */
     _writer_new_connec_exp(connection,
@@ -2248,7 +2248,7 @@ test_clear_master(void)
                  NULL);
 
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, NULL);
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con), ==, NULL);
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NULL);
 
     nmtst_assert_connection_verifies_after_normalization(connection, 0, 0);
 
@@ -3623,6 +3623,11 @@ test_roundtrip_ethtool(void)
             optname = nm_ethtool_data[ethtool_id]->optname;
             vtype   = nm_ethtool_id_get_variant_type(ethtool_id);
 
+            if (nm_ethtool_optname_is_channels(optname) || nm_ethtool_optname_is_eee(optname)) {
+                /* Not supported */
+                continue;
+            }
+
             if (NM_IN_SET(ethtool_id,
                           NM_ETHTOOL_ID_COALESCE_ADAPTIVE_RX,
                           NM_ETHTOOL_ID_COALESCE_ADAPTIVE_TX)) {
@@ -7664,9 +7669,7 @@ test_read_bridge_component(void)
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con),
-                    ==,
-                    NM_SETTING_BRIDGE_SETTING_NAME);
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BRIDGE_SETTING_NAME);
 
     s_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_BRIDGE_PORT);
     g_assert(nm_setting_bridge_port_get_hairpin_mode(s_port));
@@ -8259,7 +8262,7 @@ test_read_bond_slave(void)
 
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond0");
 
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 }
 
 static void
@@ -8315,7 +8318,7 @@ test_read_bond_port(void)
 
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond99");
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 
     s_port = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_BOND_PORT);
     g_assert_cmpuint(nm_setting_bond_port_get_queue_id(s_port), ==, 1);
@@ -8554,7 +8557,7 @@ test_read_bond_slave_ib(void)
     s_con = nmtst_connection_assert_setting(connection, NM_TYPE_SETTING_CONNECTION);
 
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "bond0");
-    g_assert_cmpstr(nm_setting_connection_get_slave_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BOND_SETTING_NAME);
 }
 
 static void
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 866b1ffd..b26bad69 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -2004,7 +2004,7 @@ test_read_bridge_component(void)
     g_assert_cmpstr(nm_setting_connection_get_id(s_con), ==, expected_id);
     g_assert_cmpstr(nm_setting_connection_get_uuid(s_con), ==, expected_uuid);
     g_assert_cmpstr(nm_setting_connection_get_master(s_con), ==, "br0");
-    g_assert(nm_setting_connection_is_slave_type(s_con, NM_SETTING_BRIDGE_SETTING_NAME));
+    g_assert_cmpstr(nm_setting_connection_get_port_type(s_con), ==, NM_SETTING_BRIDGE_SETTING_NAME);
 
     s_wired = nm_connection_get_setting_wired(connection);
     g_assert(s_wired);
@@ -2046,9 +2046,9 @@ test_write_bridge_component(void)
                  TRUE,
                  NM_SETTING_CONNECTION_TYPE,
                  NM_SETTING_WIRED_SETTING_NAME,
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  "br0",
-                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_CONNECTION_PORT_TYPE,
                  NM_SETTING_BRIDGE_SETTING_NAME,
                  NULL);
 
@@ -2357,9 +2357,9 @@ test_read_minimal_slave(void)
                                                     NM_SETTING_WIRED_SETTING_NAME,
                                                     &s_con);
     g_object_set(s_con,
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  "br0",
-                 NM_SETTING_CONNECTION_SLAVE_TYPE,
+                 NM_SETTING_CONNECTION_PORT_TYPE,
                  "bridge",
                  NULL);
     nmtst_connection_normalize(con_archetype);
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index 18525724..eb7a40a5 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -689,7 +689,7 @@ _bss_info_properties_changed(NMSupplicantInterface *self,
     v_v = nm_g_variant_lookup_value(properties, "SSID", G_VARIANT_TYPE_BYTESTRING);
     if (v_v) {
         arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
-        arr_len  = MIN(32, arr_len);
+        arr_len  = NM_MIN(32u, arr_len);
 
         /* Stupid ieee80211 layer uses <hidden> */
         if (arr_data && arr_len
@@ -765,9 +765,15 @@ _bss_info_properties_changed(NMSupplicantInterface *self,
         gboolean p_owe_transition_mode;
         gboolean p_metered;
         guint32  rate;
+        guint32  bandwidth;
 
         arr_data = g_variant_get_fixed_array(v_v, &arr_len, 1);
-        nm_wifi_utils_parse_ies(arr_data, arr_len, &rate, &p_metered, &p_owe_transition_mode);
+        nm_wifi_utils_parse_ies(arr_data,
+                                arr_len,
+                                &rate,
+                                &bandwidth,
+                                &p_metered,
+                                &p_owe_transition_mode);
         p_max_rate     = NM_MAX(p_max_rate, rate);
         p_max_rate_has = TRUE;
         g_variant_unref(v_v);
@@ -777,7 +783,8 @@ _bss_info_properties_changed(NMSupplicantInterface *self,
         else
             bss_info->rsn_flags &= ~NM_802_11_AP_SEC_KEY_MGMT_OWE_TM;
 
-        bss_info->metered = p_metered;
+        bss_info->metered   = p_metered;
+        bss_info->bandwidth = bandwidth;
     }
 
     if (p_max_rate_has)
@@ -1373,7 +1380,7 @@ _get_capability(NMSupplicantInterfacePrivate *priv, NMSupplCapType type)
     case NM_SUPPL_CAP_TYPE_AP:
         iface_value = NM_SUPPL_CAP_MASK_GET(priv->iface_capabilities, type);
         value       = NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type);
-        value       = MAX(iface_value, value);
+        value       = NM_MAX(iface_value, value);
         break;
     case NM_SUPPL_CAP_TYPE_FT:
         value = NM_SUPPL_CAP_MASK_GET(priv->global_capabilities, type);
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index fc40bade..8f2561a6 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -228,7 +228,7 @@ validate_type_keyword(const struct Opt *opt, const char *value, const guint32 le
             s++;
         }
 
-        if (nm_strv_find_first(opt->str_allowed, -1, value) < 0)
+        if (!nm_strv_contains(opt->str_allowed, -1, value))
             return FALSE;
 
         if (!s)
diff --git a/src/core/supplicant/nm-supplicant-types.h b/src/core/supplicant/nm-supplicant-types.h
index 1ae67e13..870a53c3 100644
--- a/src/core/supplicant/nm-supplicant-types.h
+++ b/src/core/supplicant/nm-supplicant-types.h
@@ -55,9 +55,9 @@ typedef enum {
     _NM_SUPPL_CAP_TYPE_NUM,
 } NMSupplCapType;
 
-#define NM_SUPPL_CAP_MASK_NO(type)   ((NMSupplCapMask) (1llu << ((type) *2u)))
-#define NM_SUPPL_CAP_MASK_YES(type)  ((NMSupplCapMask) (2llu << ((type) *2u)))
-#define NM_SUPPL_CAP_MASK_MASK(type) ((NMSupplCapMask) (3llu << ((type) *2u)))
+#define NM_SUPPL_CAP_MASK_NO(type)   ((NMSupplCapMask) (1llu << ((type) * 2u)))
+#define NM_SUPPL_CAP_MASK_YES(type)  ((NMSupplCapMask) (2llu << ((type) * 2u)))
+#define NM_SUPPL_CAP_MASK_MASK(type) ((NMSupplCapMask) (3llu << ((type) * 2u)))
 
 typedef enum {
     NM_SUPPL_CAP_MASK_NONE = 0,
@@ -179,6 +179,8 @@ typedef struct _NMSupplicantBssInfo {
 
     guint32 max_rate;
 
+    guint32 bandwidth;
+
     guint8 signal_percent;
 
     NMEtherAddr bssid;
diff --git a/src/core/tests/test-core-with-expect.c b/src/core/tests/test-core-with-expect.c
index 01510126..680843fc 100644
--- a/src/core/tests/test-core-with-expect.c
+++ b/src/core/tests/test-core-with-expect.c
@@ -617,7 +617,7 @@ test_nmp_utils_new_vlan_name(void)
             g_assert(ifname && ifname[0]);
             g_assert_cmpint(strlen(ifname),
                             ==,
-                            MIN(15, strlen(parent_names[i]) + strlen(vlan_id_s)));
+                            NM_MIN(15u, strlen(parent_names[i]) + strlen(vlan_id_s)));
             g_assert(g_str_has_suffix(ifname, vlan_id_s));
             g_assert(ifname[strlen(ifname) - strlen(vlan_id_s)] == '.');
             g_assert(strncmp(ifname, parent_names[i], strlen(ifname) - strlen(vlan_id_s)) == 0);
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index 8296d745..e90cf6e9 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -302,7 +302,7 @@ test_nm_utils_log_connection_diff(void)
     g_object_set(nm_connection_get_setting_connection(connection2),
                  NM_SETTING_CONNECTION_ID,
                  "id2",
-                 NM_SETTING_CONNECTION_MASTER,
+                 NM_SETTING_CONNECTION_CONTROLLER,
                  "master2",
                  NULL);
     nm_utils_log_connection_diff(connection,
@@ -2104,8 +2104,19 @@ do_test_stable_id_parse(const char       *stable_id,
                         NMUtilsStableType expected_stable_type,
                         const char       *expected_generated)
 {
-    gs_free char     *generated = NULL;
-    NMUtilsStableType stable_type;
+    gs_free char          *generated = NULL;
+    NMUtilsStableType      stable_type;
+    char                   ssid_bin[] = "SSID(\202)";
+    gs_unref_bytes GBytes *ssid       = g_bytes_new_static(ssid_bin, sizeof(ssid_bin) - 1);
+
+    while (TRUE) {
+        if (NM_STR_HAS_PREFIX(stable_id, "NO_SSID:")) {
+            stable_id += NM_STRLEN("NO_SSID:");
+            nm_clear_pointer(&ssid, g_bytes_unref);
+            continue;
+        }
+        break;
+    }
 
     if (expected_stable_type == NM_UTILS_STABLE_TYPE_GENERATED)
         g_assert(expected_generated);
@@ -2117,8 +2128,13 @@ do_test_stable_id_parse(const char       *stable_id,
     else
         g_assert(stable_id);
 
-    stable_type =
-        nm_utils_stable_id_parse(stable_id, "_DEVICE", "_MAC", "_BOOT", "_CONNECTION", &generated);
+    stable_type = nm_utils_stable_id_parse(stable_id,
+                                           "_DEVICE",
+                                           "_MAC",
+                                           "_BOOT",
+                                           "_CONNECTION",
+                                           ssid,
+                                           &generated);
 
     g_assert_cmpint(expected_stable_type, ==, stable_type);
 
@@ -2160,6 +2176,12 @@ test_stable_id_parse(void)
     _parse_generated("${${CONNECTION}", "${${CONNECTION}=11{_CONNECTION}");
     _parse_generated("${CONNECTION}x", "${CONNECTION}=11{_CONNECTION}x");
     _parse_generated("x${CONNECTION}", "x${CONNECTION}=11{_CONNECTION}");
+    _parse_generated("x${CONNECTION}${NETWORK_SSID}",
+                     "x${CONNECTION}=11{_CONNECTION}${NETWORK_SSID}=12{s:SSID(\\202)}");
+    _parse_generated("NO_SSID:x${CONNECTION}${NETWORK_SSID}",
+                     "x${CONNECTION}=11{_CONNECTION}${NETWORK_SSID}=13{c:_CONNECTION}");
+    _parse_generated("${NETWORK_SSID}", "${NETWORK_SSID}=12{s:SSID(\\202)}");
+    _parse_generated("NO_SSID:${NETWORK_SSID}", "${NETWORK_SSID}=13{c:_CONNECTION}");
     _parse_generated("${BOOT}x", "${BOOT}=5{_BOOT}x");
     _parse_generated("x${BOOT}", "x${BOOT}=5{_BOOT}");
     _parse_generated("x${BOOT}${CONNECTION}", "x${BOOT}=5{_BOOT}${CONNECTION}=11{_CONNECTION}");
@@ -2169,6 +2191,30 @@ test_stable_id_parse(void)
     _parse_random("${RANDOM}");
     _parse_random(" ${RANDOM}");
     _parse_random("${BOOT}${RANDOM}");
+
+    {
+        gs_free char          *str        = NULL;
+        char                   ssid_bin[] = "foo\n";
+        gs_unref_bytes GBytes *ssid       = g_bytes_new_static(ssid_bin, sizeof(ssid_bin) - 1);
+        NMUtilsStableType      stable_type;
+
+        stable_type = nm_utils_stable_id_parse_network_ssid(ssid, "uuid", FALSE, &str);
+
+        g_assert_cmpint(stable_type, ==, NM_UTILS_STABLE_TYPE_GENERATED);
+        g_assert_cmpstr(str, ==, "${NETWORK_SSID}=9{s:foo\\012}");
+
+        nm_clear_g_free(&str);
+
+        stable_type = nm_utils_stable_id_parse_network_ssid(ssid, "uuid", TRUE, &str);
+
+        g_assert_cmpint(stable_type, ==, NM_UTILS_STABLE_TYPE_GENERATED);
+        g_assert_cmpstr(str, ==, "wqLBg0FtOnCi7yYQKGDUj6CDixc");
+
+        nm_clear_g_free(&str);
+
+        str = nm_utils_stable_id_generated_complete("${NETWORK_SSID}=9{s:foo\\012}");
+        g_assert_cmpstr(str, ==, "wqLBg0FtOnCi7yYQKGDUj6CDixc");
+    }
 }
 
 /*****************************************************************************/
diff --git a/src/core/tests/test-l3cfg.c b/src/core/tests/test-l3cfg.c
index 962b2eec..6b82fe43 100644
--- a/src/core/tests/test-l3cfg.c
+++ b/src/core/tests/test-l3cfg.c
@@ -760,7 +760,7 @@ test_l3_ipv4ll(gconstpointer test_data)
         if (next_timeout_msec <= 0)
             break;
 
-        next_timeout_msec = NM_MIN(next_timeout_msec, nmtst_get_rand_uint32() % 1000u);
+        next_timeout_msec = NM_MIN(next_timeout_msec, (gint64) (nmtst_get_rand_uint32() % 1000u));
         nmtst_main_context_iterate_until(NULL, next_timeout_msec, FALSE);
         _LOGT("poll 1 intermezzo");
 
diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c
index d7102a12..3dba9ff6 100644
--- a/src/core/vpn/nm-vpn-connection.c
+++ b/src/core/vpn/nm-vpn-connection.c
@@ -1143,7 +1143,8 @@ static void
 device_state_changed(NMActiveConnection *active,
                      NMDevice           *device,
                      NMDeviceState       new_state,
-                     NMDeviceState       old_state)
+                     NMDeviceState       old_state,
+                     NMDeviceStateReason reason)
 {
     if (_service_and_connection_can_persist(NM_VPN_CONNECTION(active))) {
         if (new_state <= NM_DEVICE_STATE_DISCONNECTED || new_state == NM_DEVICE_STATE_FAILED) {