summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/devices/nm-device-bridge.c2
-rw-r--r--src/core/devices/nm-device-ethernet.c16
-rw-r--r--src/core/devices/nm-device-macsec.c13
-rw-r--r--src/core/devices/nm-device-private.h3
-rw-r--r--src/core/devices/nm-device-utils.c4
-rw-r--r--src/core/devices/nm-device-wireguard.c61
-rw-r--r--src/core/devices/nm-device.c122
-rw-r--r--src/core/devices/nm-device.h3
-rw-r--r--src/core/devices/ovs/nm-ovsdb.c105
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c38
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c2
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c12
-rw-r--r--src/core/devices/wwan/nm-modem.c41
-rw-r--r--src/core/devices/wwan/nm-modem.h2
-rw-r--r--src/core/dns/nm-dns-dnsconfd.c2
-rw-r--r--src/core/dns/nm-dns-dnsmasq.c5
-rw-r--r--src/core/dns/nm-dns-manager.c13
-rw-r--r--src/core/dns/nm-dns-systemd-resolved.c28
-rw-r--r--src/core/main-utils.c2
-rw-r--r--src/core/main.c2
-rw-r--r--src/core/ndisc/nm-lndp-ndisc.c69
-rw-r--r--src/core/nm-connectivity.c134
-rw-r--r--src/core/nm-core-utils.c158
-rw-r--r--src/core/nm-core-utils.h13
-rw-r--r--src/core/nm-l3-config-data.c42
-rw-r--r--src/core/nm-l3-config-data.h4
-rw-r--r--src/core/nm-l3cfg.c24
-rw-r--r--src/core/nm-netns.c161
-rw-r--r--src/core/nm-netns.h19
-rw-r--r--src/core/nm-policy.c5
-rw-r--r--src/core/platform/tests/monitor.c1
-rw-r--r--src/core/platform/tests/test-link.c3
-rw-r--r--src/core/platform/tests/test-route.c74
-rw-r--r--src/core/settings/nm-settings-storage.h13
-rw-r--r--src/core/settings/nm-settings.c48
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-reader.c24
-rw-r--r--src/core/settings/plugins/ifcfg-rh/nms-ifcfg-rh-writer.c9
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-utils.c1
-rw-r--r--src/core/settings/plugins/keyfile/nms-keyfile-writer.c10
-rw-r--r--src/core/settings/plugins/keyfile/tests/keyfiles/Test_Write_GSM1
-rw-r--r--src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c2
-rw-r--r--src/core/supplicant/nm-supplicant-config.c48
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c15
-rw-r--r--src/core/supplicant/nm-supplicant-interface.h1
-rw-r--r--src/core/tests/meson.build1
-rw-r--r--src/core/tests/test-netns.c69
-rw-r--r--src/core/tests/test-utils.c65
-rw-r--r--src/core/vpn/nm-vpn-connection.c51
48 files changed, 1294 insertions, 247 deletions
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index 45cdd83f..6ca3737f 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -1066,7 +1066,7 @@ attach_port(NMDevice                  *device,
 
             plat_vlans = setting_vlans_to_platform(vlans, &num_vlans);
 
-            /* Since the link was just enportd, there are no existing VLANs
+            /* Since the link was just attached, there are no existing VLANs
              * (except for the default one) and so there's no need to flush. */
 
             if (plat_vlans
diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index db1245b3..11f691de 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -15,7 +15,6 @@
 #include <linux/if_ether.h>
 
 #include "NetworkManagerUtils.h"
-#include "NetworkManagerUtils.h"
 #include "libnm-core-aux-intern/nm-libnm-core-utils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-glib-aux/nm-uuid.h"
@@ -708,6 +707,9 @@ supplicant_iface_start(NMDeviceEthernet *self)
     NMDeviceEthernetPrivate            *priv   = NM_DEVICE_ETHERNET_GET_PRIVATE(self);
     gs_unref_object NMSupplicantConfig *config = NULL;
     gs_free_error GError               *error  = NULL;
+    NMActRequest                       *request;
+    NMActiveConnection                 *controller_ac;
+    NMDevice                           *controller;
 
     config = build_supplicant_config(self, &error);
     if (!config) {
@@ -722,6 +724,16 @@ supplicant_iface_start(NMDeviceEthernet *self)
     }
 
     nm_supplicant_interface_disconnect(priv->supplicant.iface);
+
+    /* Tell the supplicant in which bridge the interface is */
+    if ((request = nm_device_get_act_request(NM_DEVICE(self)))
+        && (controller_ac = nm_active_connection_get_controller(NM_ACTIVE_CONNECTION(request)))
+        && (controller = nm_active_connection_get_device(controller_ac))
+        && nm_device_get_device_type(controller) == NM_DEVICE_TYPE_BRIDGE) {
+        nm_supplicant_interface_set_bridge(priv->supplicant.iface, nm_device_get_iface(controller));
+    } else
+        nm_supplicant_interface_set_bridge(priv->supplicant.iface, NULL);
+
     nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self);
     return TRUE;
 }
@@ -1901,7 +1913,7 @@ get_ip_method_auto(NMDevice *device, int addr_family)
         /* We cannot do DHCPv4 on a PPP link, instead we get "auto" IP addresses
          * by pppd. Return "manual" here, which has the suitable effect to a
          * (zero) manual addresses in addition. */
-        return NM_SETTING_IP6_CONFIG_METHOD_MANUAL;
+        return NM_SETTING_IP4_CONFIG_METHOD_MANUAL;
     }
 
     return NM_SETTING_IP6_CONFIG_METHOD_AUTO;
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 1659ea05..eb39cb2a 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -440,6 +440,9 @@ supplicant_iface_start(NMDeviceMacsec *self)
     NMDeviceMacsecPrivate              *priv   = NM_DEVICE_MACSEC_GET_PRIVATE(self);
     gs_unref_object NMSupplicantConfig *config = NULL;
     gs_free_error GError               *error  = NULL;
+    NMActRequest                       *request;
+    NMActiveConnection                 *controller_ac;
+    NMDevice                           *controller;
 
     config = build_supplicant_config(self, &error);
     if (!config) {
@@ -452,6 +455,16 @@ supplicant_iface_start(NMDeviceMacsec *self)
     }
 
     nm_supplicant_interface_disconnect(priv->supplicant.iface);
+
+    /* Tell the supplicant in which bridge the interface is */
+    if ((request = nm_device_get_act_request(NM_DEVICE(self)))
+        && (controller_ac = nm_active_connection_get_controller(NM_ACTIVE_CONNECTION(request)))
+        && (controller = nm_active_connection_get_device(controller_ac))
+        && nm_device_get_device_type(controller) == NM_DEVICE_TYPE_BRIDGE) {
+        nm_supplicant_interface_set_bridge(priv->supplicant.iface, nm_device_get_iface(controller));
+    } else
+        nm_supplicant_interface_set_bridge(priv->supplicant.iface, NULL);
+
     nm_supplicant_interface_assoc(priv->supplicant.iface, config, supplicant_iface_assoc_cb, self);
     return TRUE;
 }
diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
index 2568d9f1..6d828597 100644
--- a/src/core/devices/nm-device-private.h
+++ b/src/core/devices/nm-device-private.h
@@ -115,9 +115,6 @@ gboolean nm_device_sysctl_ip_conf_set(NMDevice   *self,
 
 NML3ConfigData *nm_device_create_l3_config_data(NMDevice *self, NMIPConfigSource source);
 
-NML3ConfigData *nm_device_create_l3_config_data_from_connection(NMDevice     *self,
-                                                                NMConnection *connection);
-
 void nm_device_ip_method_dhcp4_start(NMDevice *self);
 
 void nm_device_ip_method_autoconf6_start(NMDevice *self);
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index 3e86e2e8..be1de3ea 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -143,7 +143,9 @@ NM_UTILS_LOOKUP_STR_DEFINE(
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_SETTINGS,
                              "unmanaged-user-settings"),
     NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_UNMANAGED_USER_UDEV, "unmanaged-user-udev"),
-    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NETWORKING_OFF, "networking-off"), );
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_NETWORKING_OFF, "networking-off"),
+    NM_UTILS_LOOKUP_STR_ITEM(NM_DEVICE_STATE_REASON_MODEM_NO_OPERATOR_CODE,
+                             "modem-no-operator-code"), );
 
 NM_UTILS_LOOKUP_STR_DEFINE(nm_device_mtu_source_to_string,
                            NMDeviceMtuSource,
diff --git a/src/core/devices/nm-device-wireguard.c b/src/core/devices/nm-device-wireguard.c
index 299e3b30..d98059ba 100644
--- a/src/core/devices/nm-device-wireguard.c
+++ b/src/core/devices/nm-device-wireguard.c
@@ -1672,6 +1672,57 @@ act_stage2_config(NMDevice *device, NMDeviceStateReason *out_failure_reason)
     return ret;
 }
 
+static gboolean
+skip_peer_route(const NMIPAddr    *peer_addr,
+                guint              peer_addr_prefix,
+                int                addr_family,
+                NMSettingIPConfig *s_ip)
+{
+    guint num_addresses;
+    guint i;
+
+    /*
+     * If the allowed-ip subnet is already reachable on the interface via the
+     * prefix route of a static IP address, skip adding the peer route.
+     * We don't want to override the prefix route with a new one because the
+     * prefix route also specifies the correct source IP address.
+     *
+     * wg-quick does something similar here:
+     * https://git.zx2c4.com/wireguard-tools/tree/src/wg-quick/linux.bash?h=v1.0.20250521#n177
+     * The condition in wg-quick is a bit different because it checks that no
+     * duplicate route exists on the interface. We can't do exactly the same
+     * because here we don't have visibility on all the platform routes.
+     */
+
+    if (!s_ip)
+        return FALSE;
+
+    num_addresses = nm_setting_ip_config_get_num_addresses(s_ip);
+    for (i = 0; i < num_addresses; i++) {
+        NMIPAddr     setting_addr;
+        NMIPAddr     peer_addr_tmp;
+        guint        setting_prefix;
+        NMIPAddress *a;
+
+        peer_addr_tmp = *peer_addr;
+
+        a = nm_setting_ip_config_get_address(s_ip, i);
+        nm_ip_address_get_address_binary(a, &setting_addr);
+        setting_prefix = nm_ip_address_get_prefix(a);
+
+        if (setting_prefix > peer_addr_prefix)
+            continue;
+
+        nm_ip_addr_clear_host_address(addr_family, &setting_addr, NULL, setting_prefix);
+        nm_ip_addr_clear_host_address(addr_family, &peer_addr_tmp, NULL, setting_prefix);
+
+        if (nm_ip_addr_equal(addr_family, &peer_addr_tmp, &setting_addr))
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
 static const NML3ConfigData *
 _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
 {
@@ -1738,6 +1789,7 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
 
         n_aips = nm_wireguard_peer_get_allowed_ips_len(peer);
         for (j = 0; j < n_aips; j++) {
+            NMSettingIPConfig *s_ip;
             NMPlatformIPXRoute rt;
             NMIPAddr           addrbin;
             const char        *aip;
@@ -1745,7 +1797,8 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
             int                prefix;
             guint32            rtable_coerced;
 
-            aip = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
+            aip  = nm_wireguard_peer_get_allowed_ip(peer, j, &valid);
+            s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
 
             if (!valid || !nm_inet_parse_with_prefix_bin(addr_family, aip, NULL, &addrbin, &prefix))
                 continue;
@@ -1754,9 +1807,6 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
                 prefix = (addr_family == AF_INET) ? 32 : 128;
 
             if (prefix == 0) {
-                NMSettingIPConfig *s_ip;
-
-                s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
                 if (nm_setting_ip_config_get_never_default(s_ip))
                     continue;
             }
@@ -1769,6 +1819,9 @@ _get_dev2_ip_config(NMDeviceWireGuard *self, int addr_family)
 
             nm_ip_addr_clear_host_address(addr_family, &addrbin, NULL, prefix);
 
+            if (skip_peer_route(&addrbin, prefix, addr_family, s_ip))
+                continue;
+
             rtable_coerced = route_table_coerced;
 
             if (prefix == 0 && auto_default_route_enabled) {
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index b0a3f780..46fb2339 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -113,6 +113,19 @@ typedef enum {
     RELEASE_PORT_TYPE_CONFIG_FORCE,
 } ReleasePortType;
 
+/**
+ * CleanupType:
+ * @CLEANUP_TYPE_KEEP: Cleanup internally but keep the real device's config. This is
+ *   often used when moving a partially managed device to "unmanaged" (but not only).
+ * @CLEANUP_TYPE_REMOVED: The device suddently disappeared. Cleanup internally but don't
+ *   make any action on the real device at all, as it no longer exists.
+ * @CLEANUP_TYPE_DECONFIGURE: Also deconfigure the real device. This is the typical
+ *   action when a connection or device is set to "down", or fully managed devices
+ *   moved to "unmanaged".
+ * @CLEANUP_TYPE_KEEP_REAPPLY: Like %CLEANUP_TYPE_KEEP, but indicating that it's a
+ *   reapply. Some special actions can be done if we're doing a reapply, like keeping
+ *   the existing DHCP lease, for example.
+ */
 typedef enum {
     CLEANUP_TYPE_KEEP,
     CLEANUP_TYPE_REMOVED,
@@ -265,11 +278,11 @@ typedef struct {
     NMDeviceIPState state;
     union {
         struct {
-            NMDnsMasqManager      *dnsmasq_manager;
-            NMNetnsSharedIPHandle *shared_ip_handle;
-            NMFirewallConfig      *firewall_config;
-            gulong                 dnsmasq_state_id;
-            const NML3ConfigData  *l3cd;
+            NMDnsMasqManager     *dnsmasq_manager;
+            NMNetnsIPReservation *ip_reservation;
+            NMFirewallConfig     *firewall_config;
+            gulong                dnsmasq_state_id;
+            const NML3ConfigData *l3cd;
         } v4;
         struct {
         } v6;
@@ -1411,14 +1424,12 @@ _prop_get_ipvx_routed_dns(NMDevice *self, int addr_family)
 }
 
 static NMSettingConnectionMdns
-_prop_get_connection_mdns(NMDevice *self)
+_prop_get_connection_mdns(NMDevice *self, NMConnection *connection)
 {
-    NMConnection           *connection;
     NMSettingConnectionMdns mdns = NM_SETTING_CONNECTION_MDNS_DEFAULT;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_MDNS_DEFAULT);
 
-    connection = nm_device_get_applied_connection(self);
     if (connection)
         mdns = nm_setting_connection_get_mdns(nm_connection_get_setting_connection(connection));
     if (mdns != NM_SETTING_CONNECTION_MDNS_DEFAULT)
@@ -1453,14 +1464,12 @@ _prop_get_sriov_preserve_on_down(NMDevice *self, NMSettingSriov *s_sriov)
 }
 
 static NMSettingConnectionLlmnr
-_prop_get_connection_llmnr(NMDevice *self)
+_prop_get_connection_llmnr(NMDevice *self, NMConnection *connection)
 {
-    NMConnection            *connection;
     NMSettingConnectionLlmnr llmnr = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_LLMNR_DEFAULT);
 
-    connection = nm_device_get_applied_connection(self);
     if (connection)
         llmnr = nm_setting_connection_get_llmnr(nm_connection_get_setting_connection(connection));
     if (llmnr != NM_SETTING_CONNECTION_LLMNR_DEFAULT)
@@ -1475,14 +1484,12 @@ _prop_get_connection_llmnr(NMDevice *self)
 }
 
 static NMSettingConnectionDnsOverTls
-_prop_get_connection_dns_over_tls(NMDevice *self)
+_prop_get_connection_dns_over_tls(NMDevice *self, NMConnection *connection)
 {
-    NMConnection                 *connection;
     NMSettingConnectionDnsOverTls dns_over_tls = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT);
 
-    connection = nm_device_get_applied_connection(self);
     if (connection)
         dns_over_tls = nm_setting_connection_get_dns_over_tls(
             nm_connection_get_setting_connection(connection));
@@ -1497,15 +1504,33 @@ _prop_get_connection_dns_over_tls(NMDevice *self)
                                                        NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT);
 }
 
+static NMSettingConnectionDnssec
+_prop_get_connection_dnssec(NMDevice *self, NMConnection *connection)
+{
+    NMSettingConnectionDnssec dnssec = NM_SETTING_CONNECTION_DNSSEC_DEFAULT;
+
+    g_return_val_if_fail(NM_IS_DEVICE(self), NM_SETTING_CONNECTION_DNSSEC_DEFAULT);
+
+    if (connection)
+        dnssec = nm_setting_connection_get_dnssec(nm_connection_get_setting_connection(connection));
+    if (dnssec != NM_SETTING_CONNECTION_DNSSEC_DEFAULT)
+        return dnssec;
+
+    return nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
+                                                       NM_CON_DEFAULT("connection.dnssec"),
+                                                       self,
+                                                       NM_SETTING_CONNECTION_DNSSEC_NO,
+                                                       NM_SETTING_CONNECTION_DNSSEC_YES,
+                                                       NM_SETTING_CONNECTION_DNSSEC_DEFAULT);
+}
+
 static NMMptcpFlags
-_prop_get_connection_mptcp_flags(NMDevice *self)
+_prop_get_connection_mptcp_flags(NMDevice *self, NMConnection *connection)
 {
-    NMConnection *connection;
-    NMMptcpFlags  mptcp_flags = NM_MPTCP_FLAGS_NONE;
+    NMMptcpFlags mptcp_flags = NM_MPTCP_FLAGS_NONE;
 
     g_return_val_if_fail(NM_IS_DEVICE(self), NM_MPTCP_FLAGS_DISABLED);
 
-    connection = nm_device_get_applied_connection(self);
     if (connection) {
         mptcp_flags =
             nm_setting_connection_get_mptcp_flags(nm_connection_get_setting_connection(connection));
@@ -2471,16 +2496,14 @@ _prop_get_ipv4_dhcp_vendor_class_identifier(NMDevice *self, NMSettingIP4Config *
 }
 
 static NMSettingIP6ConfigPrivacy
-_prop_get_ipv6_ip6_privacy(NMDevice *self)
+_prop_get_ipv6_ip6_privacy(NMDevice *self, NMConnection *connection)
 {
     NMSettingIP6ConfigPrivacy ip6_privacy;
-    NMConnection             *connection;
 
     g_return_val_if_fail(self, NM_SETTING_IP6_CONFIG_PRIVACY_UNKNOWN);
 
     /* 1.) First look at the per-connection setting. If it is not -1 (unknown),
      * use it. */
-    connection = nm_device_get_applied_connection(self);
     if (connection) {
         NMSettingIPConfig *s_ip6 = nm_connection_get_setting_ip6_config(connection);
 
@@ -3613,11 +3636,12 @@ nm_device_create_l3_config_data_from_connection(NMDevice *self, NMConnection *co
 
     l3cd =
         nm_l3_config_data_new_from_connection(nm_device_get_multi_index(self), ifindex, connection);
-    nm_l3_config_data_set_mdns(l3cd, _prop_get_connection_mdns(self));
-    nm_l3_config_data_set_llmnr(l3cd, _prop_get_connection_llmnr(self));
-    nm_l3_config_data_set_dns_over_tls(l3cd, _prop_get_connection_dns_over_tls(self));
-    nm_l3_config_data_set_ip6_privacy(l3cd, _prop_get_ipv6_ip6_privacy(self));
-    nm_l3_config_data_set_mptcp_flags(l3cd, _prop_get_connection_mptcp_flags(self));
+    nm_l3_config_data_set_mdns(l3cd, _prop_get_connection_mdns(self, connection));
+    nm_l3_config_data_set_llmnr(l3cd, _prop_get_connection_llmnr(self, connection));
+    nm_l3_config_data_set_dns_over_tls(l3cd, _prop_get_connection_dns_over_tls(self, connection));
+    nm_l3_config_data_set_dnssec(l3cd, _prop_get_connection_dnssec(self, connection));
+    nm_l3_config_data_set_ip6_privacy(l3cd, _prop_get_ipv6_ip6_privacy(self, connection));
+    nm_l3_config_data_set_mptcp_flags(l3cd, _prop_get_connection_mptcp_flags(self, connection));
     return l3cd;
 }
 
@@ -12974,7 +12998,7 @@ _dev_ipac6_start(NMDevice *self)
             .router_solicitations         = router_solicitations,
             .router_solicitation_interval = router_solicitation_interval,
             .ra_timeout                   = ra_timeout,
-            .ip6_privacy                  = _prop_get_ipv6_ip6_privacy(self),
+            .ip6_privacy                  = _prop_get_ipv6_ip6_privacy(self, connection),
         };
 
         priv->ipac6_data.ndisc = nm_lndp_ndisc_new(&config);
@@ -13161,7 +13185,6 @@ _dev_addrgenmode6_set(NMDevice *self, guint8 addr_gen_mode)
     if (!priv->addrgenmode6_data.previous_mode_has) {
         priv->addrgenmode6_data.previous_mode_has = TRUE;
         priv->addrgenmode6_data.previous_mode_val = cur_addr_gen_mode;
-        nm_assert(priv->addrgenmode6_data.previous_mode_val == cur_addr_gen_mode);
     }
 
     _LOGD_ip(AF_INET6,
@@ -13650,7 +13673,7 @@ _dev_ipsharedx_cleanup(NMDevice *self, int addr_family)
             nm_clear_pointer(&priv->ipshared_data_4.v4.firewall_config, nm_firewall_config_free);
         }
 
-        nm_clear_pointer(&priv->ipshared_data_4.v4.shared_ip_handle, nm_netns_shared_ip_release);
+        nm_clear_pointer(&priv->ipshared_data_4.v4.ip_reservation, nm_netns_ip_reservation_release);
         nm_clear_l3cd(&priv->ipshared_data_4.v4.l3cd);
 
         _dev_l3_register_l3cds_set_one(self, L3_CONFIG_DATA_TYPE_SHARED_4, NULL, FALSE);
@@ -13684,13 +13707,14 @@ _dev_ipshared4_new_l3cd(NMDevice *self, NMConnection *connection, NMPlatformIP4A
 
         nm_ip_address_get_address_binary(user, &a);
         nm_platform_ip4_address_set_addr(&address, a, nm_ip_address_get_prefix(user));
-        nm_clear_pointer(&priv->ipshared_data_4.v4.shared_ip_handle, nm_netns_shared_ip_release);
+        nm_clear_pointer(&priv->ipshared_data_4.v4.ip_reservation, nm_netns_ip_reservation_release);
     } else {
-        if (!priv->ipshared_data_4.v4.shared_ip_handle)
-            priv->ipshared_data_4.v4.shared_ip_handle =
-                nm_netns_shared_ip_reserve(nm_device_get_netns(self));
+        if (!priv->ipshared_data_4.v4.ip_reservation)
+            priv->ipshared_data_4.v4.ip_reservation =
+                nm_netns_ip_reservation_get(nm_device_get_netns(self),
+                                            NM_NETNS_IP_RESERVATION_TYPE_SHARED4);
         nm_platform_ip4_address_set_addr(&address,
-                                         priv->ipshared_data_4.v4.shared_ip_handle->addr,
+                                         priv->ipshared_data_4.v4.ip_reservation->addr,
                                          24);
     }
 
@@ -14283,6 +14307,7 @@ can_reapply_change(NMDevice   *self,
                                                  NM_SETTING_CONNECTION_MDNS,
                                                  NM_SETTING_CONNECTION_LLMNR,
                                                  NM_SETTING_CONNECTION_DNS_OVER_TLS,
+                                                 NM_SETTING_CONNECTION_DNSSEC,
                                                  NM_SETTING_CONNECTION_MPTCP_FLAGS,
                                                  NM_SETTING_CONNECTION_WAIT_ACTIVATION_DELAY);
     }
@@ -14541,6 +14566,7 @@ check_and_reapply_connection(NMDevice            *self,
                 NM_SETTING_CONNECTION_MDNS,
                 NM_SETTING_CONNECTION_LLMNR,
                 NM_SETTING_CONNECTION_DNS_OVER_TLS,
+                NM_SETTING_CONNECTION_DNSSEC,
                 NM_SETTING_CONNECTION_MPTCP_FLAGS)) {
             priv->ip_data_4.do_reapply = TRUE;
             priv->ip_data_6.do_reapply = TRUE;
@@ -17285,6 +17311,25 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
         /* controller: release ports */
         nm_device_controller_release_ports_all(self);
 
+        /* port: detach from controller */
+        if (priv->controller) {
+            nm_device_controller_release_port(priv->controller,
+                                              self,
+                                              RELEASE_PORT_TYPE_CONFIG,
+                                              reason);
+        }
+    }
+
+    /* port: mark no longer attached */
+    if (priv->controller && priv->ifindex > 0
+        && nm_platform_link_get_controller(nm_device_get_platform(self), priv->ifindex) <= 0) {
+        nm_device_controller_release_port(priv->controller,
+                                          self,
+                                          RELEASE_PORT_TYPE_NO_CONFIG,
+                                          NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
+    }
+
+    if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
         /* Take out any entries in the routing table and any IP address the device had. */
         if (ifindex > 0) {
             NMPlatform *platform = nm_device_get_platform(self);
@@ -17308,15 +17353,6 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
     if (ifindex > 0)
         nm_platform_ip4_dev_route_blacklist_set(nm_device_get_platform(self), ifindex, NULL);
 
-    /* port: mark no longer attached */
-    if (priv->controller && priv->ifindex > 0
-        && nm_platform_link_get_controller(nm_device_get_platform(self), priv->ifindex) <= 0) {
-        nm_device_controller_release_port(priv->controller,
-                                          self,
-                                          RELEASE_PORT_TYPE_NO_CONFIG,
-                                          NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED);
-    }
-
     lldp_setup(self, NM_TERNARY_FALSE);
 
     nm_device_update_metered(self);
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index 8632944a..2f287953 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -853,4 +853,7 @@ void nm_routing_rules_sync(NMConnection *applied_connection,
                            NMDevice *self,
                            NMNetns  *netns);
 
+NML3ConfigData *nm_device_create_l3_config_data_from_connection(NMDevice     *self,
+                                                                NMConnection *connection);
+
 #endif /* __NETWORKMANAGER_DEVICE_H__ */
diff --git a/src/core/devices/ovs/nm-ovsdb.c b/src/core/devices/ovs/nm-ovsdb.c
index 1c9484c4..cb67848e 100644
--- a/src/core/devices/ovs/nm-ovsdb.c
+++ b/src/core/devices/ovs/nm-ovsdb.c
@@ -1890,7 +1890,7 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
         == -1) {
         /* This doesn't really have to be an error; the key might
          * be missing if there really are no bridges present. */
-        _LOGD("Bad update: %s", json_error.text);
+        _LOGD("monitor: bad update: %s", json_error.text);
     }
 
     if (ovs) {
@@ -1936,12 +1936,12 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                                              &unused))
                 continue;
 
-            _LOGT("obj[iface:%s]: removed an '%s' interface: %s%s%s",
-                  key,
-                  ovs_interface->type,
+            _LOGT("monitor: %s: interface removed: type=%s, obj[iface:%s]%s%s",
                   ovs_interface->name,
+                  ovs_interface->type,
+                  key,
                   NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
-                                       ", ",
+                                       ", connection=",
                                        ovs_interface->connection_uuid,
                                        ""));
             _signal_emit_device_removed(self,
@@ -1989,17 +1989,18 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 gs_free char *strtmp1 = NULL;
                 gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[iface:%s]: changed an '%s' interface: %s%s%s, external-ids=%s, "
-                      "other-config=%s",
-                      key,
-                      type,
-                      ovs_interface->name,
-                      NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
-                                           ", ",
-                                           ovs_interface->connection_uuid,
-                                           ""),
-                      (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
-                      (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
+                _LOGT(
+                    "monitor: %s: interface changed: type=%s, obj[iface:%s]%s%s, external-ids=%s, "
+                    "other-config=%s",
+                    ovs_interface->name,
+                    type,
+                    key,
+                    NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                         ", connection=",
+                                         ovs_interface->connection_uuid,
+                                         ""),
+                    (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                    (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             }
         } else {
             gs_free char *strtmp1 = NULL;
@@ -2015,17 +2016,17 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->interfaces, ovs_interface);
-            _LOGT(
-                "obj[iface:%s]: added an '%s' interface: %s%s%s, external-ids=%s, other-config=%s",
-                key,
-                ovs_interface->type,
-                ovs_interface->name,
-                NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
-                                     ", ",
-                                     ovs_interface->connection_uuid,
-                                     ""),
-                (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
-                (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
+            _LOGT("monitor: %s: interface added: type=%s, obj[iface:%s]%s%s, external-ids=%s, "
+                  "other-config=%s",
+                  ovs_interface->name,
+                  ovs_interface->type,
+                  key,
+                  NM_PRINT_FMT_QUOTED2(ovs_interface->connection_uuid,
+                                       ", connection=",
+                                       ovs_interface->connection_uuid,
+                                       ""),
+                  (strtmp1 = _strdict_to_string(ovs_interface->external_ids)),
+                  (strtmp2 = _strdict_to_string(ovs_interface->other_config)));
             _signal_emit_device_added(self,
                                       ovs_interface->name,
                                       NM_DEVICE_TYPE_OVS_INTERFACE,
@@ -2071,11 +2072,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
             if (!g_hash_table_steal_extended(priv->ports, &key, (gpointer *) &ovs_port, &unused))
                 continue;
 
-            _LOGT("obj[port:%s]: removed a port: %s%s%s",
-                  key,
+            _LOGT("monitor: %s: port removed: obj[port:%s]%s%s",
                   ovs_port->name,
+                  key,
                   NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
-                                       ", ",
+                                       ", connection=",
                                        ovs_port->connection_uuid,
                                        ""));
             _signal_emit_device_removed(self, ovs_port->name, NM_DEVICE_TYPE_OVS_PORT, NULL);
@@ -2122,15 +2123,16 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 gs_free char *strtmp1 = NULL;
                 gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[port:%s]: changed a port: %s%s%s, external-ids=%s, other-config=%s",
-                      key,
-                      ovs_port->name,
-                      NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
-                                           ", ",
-                                           ovs_port->connection_uuid,
-                                           ""),
-                      (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
-                      (strtmp2 = _strdict_to_string(ovs_port->other_config)));
+                _LOGT(
+                    "monitor: %s: port changed: obj[port:%s]%s%s, external-ids=%s, other-config=%s",
+                    ovs_port->name,
+                    key,
+                    NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
+                                         ", connection=",
+                                         ovs_port->connection_uuid,
+                                         ""),
+                    (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
+                    (strtmp2 = _strdict_to_string(ovs_port->other_config)));
             }
         } else {
             gs_free char *strtmp1 = NULL;
@@ -2146,11 +2148,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->ports, ovs_port);
-            _LOGT("obj[port:%s]: added a port: %s%s%s, external-ids=%s, other-config=%s",
-                  key,
+            _LOGT("monitor: %s: port added: obj[port:%s]%s%s, external-ids=%s, other-config=%s",
                   ovs_port->name,
+                  key,
                   NM_PRINT_FMT_QUOTED2(ovs_port->connection_uuid,
-                                       ", ",
+                                       ", connection=",
                                        ovs_port->connection_uuid,
                                        ""),
                   (strtmp1 = _strdict_to_string(ovs_port->external_ids)),
@@ -2192,11 +2194,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                                              &unused))
                 continue;
 
-            _LOGT("obj[bridge:%s]: removed a bridge: %s%s%s",
-                  key,
+            _LOGT("monitor: %s: bridge removed: obj[bridge:%s]%s%s",
                   ovs_bridge->name,
+                  key,
                   NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
-                                       ", ",
+                                       ", connection=",
                                        ovs_bridge->connection_uuid,
                                        ""));
             _signal_emit_device_removed(self, ovs_bridge->name, NM_DEVICE_TYPE_OVS_BRIDGE, NULL);
@@ -2243,11 +2245,12 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 gs_free char *strtmp1 = NULL;
                 gs_free char *strtmp2 = NULL;
 
-                _LOGT("obj[bridge:%s]: changed a bridge: %s%s%s, external-ids=%s, other-config=%s",
-                      key,
+                _LOGT("monitor: %s: bridge changed: obj[bridge:%s]%s%s, external-ids=%s, "
+                      "other-config=%s",
                       ovs_bridge->name,
+                      key,
                       NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
-                                           ", ",
+                                           ", connection=",
                                            ovs_bridge->connection_uuid,
                                            ""),
                       (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
@@ -2267,11 +2270,11 @@ ovsdb_got_update(NMOvsdb *self, json_t *msg)
                 .other_config    = g_steal_pointer(&other_config_arr),
             };
             g_hash_table_add(priv->bridges, ovs_bridge);
-            _LOGT("obj[bridge:%s]: added a bridge: %s%s%s, external-ids=%s, other-config=%s",
-                  key,
+            _LOGT("monitor: %s: bridge added: obj[bridge:%s]%s%s, external-ids=%s, other-config=%s",
                   ovs_bridge->name,
+                  key,
                   NM_PRINT_FMT_QUOTED2(ovs_bridge->connection_uuid,
-                                       ", ",
+                                       ", connection=",
                                        ovs_bridge->connection_uuid,
                                        ""),
                   (strtmp1 = _strdict_to_string(ovs_bridge->external_ids)),
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 148caa11..b41ed5e1 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -191,6 +191,9 @@ static void supplicant_iface_notify_p2p_available(NMSupplicantInterface *iface,
                                                   GParamSpec            *pspec,
                                                   NMDeviceWifi          *self);
 
+static void supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface,
+                                                        NMDeviceWifi          *self);
+
 static void periodic_update(NMDeviceWifi *self);
 
 static void ap_add_remove(NMDeviceWifi *self,
@@ -624,6 +627,10 @@ supplicant_interface_acquire_cb(NMSupplicantManager         *supplicant_manager,
                      "notify::" NM_SUPPLICANT_INTERFACE_P2P_AVAILABLE,
                      G_CALLBACK(supplicant_iface_notify_p2p_available),
                      self);
+    g_signal_connect(priv->sup_iface,
+                     NM_SUPPLICANT_INTERFACE_PSK_MISMATCH,
+                     G_CALLBACK(supplicant_iface_notify_wpa_psk_mismatch_cb),
+                     self);
 
     _scan_notify_is_scanning(self);
 
@@ -2398,6 +2405,9 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi              *self,
 
     g_return_val_if_fail(new_state == NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, FALSE);
 
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG)
+        return FALSE;
+
     req = nm_device_get_act_request(NM_DEVICE(self));
     g_return_val_if_fail(req != NULL, FALSE);
 
@@ -2841,6 +2851,34 @@ handle_auth_or_fail(NMDeviceWifi *self, NMActRequest *req, gboolean new_secrets)
     return TRUE;
 }
 
+static void
+supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface, NMDeviceWifi *self)
+{
+    NMDevice     *device = NM_DEVICE(self);
+    NMActRequest *req;
+    const char   *setting_name = NM_SETTING_WIRELESS_SECURITY_SETTING_NAME;
+
+    if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG)
+        return;
+
+    _LOGI(LOGD_DEVICE | LOGD_WIFI,
+          "Activation: (wifi) psk mismatch reported by supplicant, asking for new key");
+
+    req = nm_device_get_act_request(NM_DEVICE(self));
+    g_return_if_fail(req != NULL);
+
+    nm_act_request_clear_secrets(req);
+
+    cleanup_association_attempt(self, TRUE);
+    nm_device_state_changed(device,
+                            NM_DEVICE_STATE_NEED_AUTH,
+                            NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT);
+    wifi_secrets_get_secrets(self,
+                             setting_name,
+                             NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION
+                                 | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW);
+}
+
 /*
  * supplicant_connection_timeout_cb
  *
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 76a342e2..bb2e056d 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -684,7 +684,7 @@ iwd_config_write(GKeyFile              *config,
      * in the last few filename characters -- it cannot end in .open, .psk
      * or .8021x.
      */
-    return nm_utils_file_set_contents(filepath, data, length, 0600, times, NULL, error);
+    return nm_utils_file_set_contents(filepath, data, length, 0600, times, NULL, NULL, error);
 }
 
 static const char *
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index 018e5306..4bd0a45c 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -508,8 +508,9 @@ find_gsm_apn_cb(const char   *apn,
 static gboolean
 try_create_connect_properties(NMModemBroadband *self)
 {
-    NMModemBroadbandPrivate *priv = NM_MODEM_BROADBAND_GET_PRIVATE(self);
-    ConnectContext          *ctx  = priv->ctx;
+    NMModemBroadbandPrivate *priv        = NM_MODEM_BROADBAND_GET_PRIVATE(self);
+    ConnectContext          *ctx         = priv->ctx;
+    NMDeviceStateReason      fail_reason = NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED;
 
     if (MODEM_CAPS_3GPP(ctx->caps)) {
         NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(ctx->connection);
@@ -522,7 +523,7 @@ try_create_connect_properties(NMModemBroadband *self)
             if (s_gsm)
                 network_id = nm_setting_gsm_get_network_id(s_gsm);
             if (!network_id) {
-                if (mm_modem_get_state(self->_priv.modem_iface) < MM_MODEM_STATE_REGISTERED)
+                if (mm_modem_get_state(self->_priv.modem_iface) != MM_MODEM_STATE_REGISTERED)
                     return FALSE;
                 modem_3gpp = mm_object_get_modem_3gpp(priv->modem_object);
                 network_id = mm_modem_3gpp_get_operator_code(modem_3gpp);
@@ -530,6 +531,7 @@ try_create_connect_properties(NMModemBroadband *self)
             if (!network_id) {
                 _LOGW("failed to connect '%s': unable to determine the network id",
                       nm_connection_get_id(ctx->connection));
+                fail_reason = NM_DEVICE_STATE_REASON_MODEM_NO_OPERATOR_CODE;
                 goto out;
             }
 
@@ -558,7 +560,7 @@ try_create_connect_properties(NMModemBroadband *self)
     }
 
 out:
-    nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED);
+    nm_modem_emit_prepare_result(NM_MODEM(self), FALSE, fail_reason);
     connect_context_clear(self);
     return TRUE;
 }
@@ -1649,6 +1651,8 @@ nm_modem_broadband_new(GObject *object, GError **error)
                         driver,
                         NM_MODEM_OPERATOR_CODE,
                         operator_code,
+                        NM_MODEM_DEVICE_UID,
+                        mm_modem_get_device(modem_iface),
                         NULL);
 }
 
diff --git a/src/core/devices/wwan/nm-modem.c b/src/core/devices/wwan/nm-modem.c
index c4852ea2..9d8f61c5 100644
--- a/src/core/devices/wwan/nm-modem.c
+++ b/src/core/devices/wwan/nm-modem.c
@@ -39,7 +39,8 @@ NM_GOBJECT_PROPERTIES_DEFINE(NMModem,
                              PROP_IP_TYPES,
                              PROP_SIM_OPERATOR_ID,
                              PROP_OPERATOR_CODE,
-                             PROP_APN, );
+                             PROP_APN,
+                             PROP_DEVICE_UID, );
 
 enum {
     PPP_STATS,
@@ -78,6 +79,7 @@ typedef struct _NMModemPrivate {
     char           *sim_operator_id;
     char           *operator_code;
     char           *apn;
+    char           *device_uid;
 
     NMPPPManager *ppp_manager;
     NMPppMgr     *ppp_mgr;
@@ -618,6 +620,12 @@ nm_modem_get_apn(NMModem *self)
     return NM_MODEM_GET_PRIVATE(self)->apn;
 }
 
+const char *
+nm_modem_get_device_uid(NMModem *self)
+{
+    return NM_MODEM_GET_PRIVATE(self)->device_uid;
+}
+
 /*****************************************************************************/
 
 static void
@@ -1121,6 +1129,22 @@ nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GE
             }
         }
 
+        str = nm_setting_gsm_get_device_uid(s_gsm);
+        if (str) {
+            if (!priv->device_uid) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "GSM profile has device-uid, device does not");
+                return FALSE;
+            }
+            if (!nm_streq(str, priv->device_uid)) {
+                nm_utils_error_set_literal(error,
+                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
+                                           "device has differing device-uid than GSM profile");
+                return FALSE;
+            }
+        }
+
         /* SIM properties may not be available before the SIM is unlocked, so
          * to ensure that autoconnect works, the connection's SIM properties
          * are only compared if present on the device.
@@ -1644,6 +1668,9 @@ get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
     case PROP_APN:
         g_value_set_string(value, priv->apn);
         break;
+    case PROP_DEVICE_UID:
+        g_value_set_string(value, priv->device_uid);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -1699,6 +1726,10 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
         /* construct-only */
         priv->operator_code = g_value_dup_string(value);
         break;
+    case PROP_DEVICE_UID:
+        /* construct-only */
+        priv->device_uid = g_value_dup_string(value);
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
         break;
@@ -1758,6 +1789,7 @@ finalize(GObject *object)
     g_free(priv->sim_operator_id);
     g_free(priv->operator_code);
     g_free(priv->apn);
+    g_free(priv->device_uid);
 
     G_OBJECT_CLASS(nm_modem_parent_class)->finalize(object);
 }
@@ -1863,6 +1895,13 @@ nm_modem_class_init(NMModemClass *klass)
     obj_properties[PROP_APN] =
         g_param_spec_string(NM_MODEM_APN, "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
+    obj_properties[PROP_DEVICE_UID] =
+        g_param_spec_string(NM_MODEM_DEVICE_UID,
+                            "",
+                            "",
+                            NULL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
     signals[PPP_STATS] = g_signal_new(NM_MODEM_PPP_STATS,
diff --git a/src/core/devices/wwan/nm-modem.h b/src/core/devices/wwan/nm-modem.h
index 021d77b2..1f54b0b6 100644
--- a/src/core/devices/wwan/nm-modem.h
+++ b/src/core/devices/wwan/nm-modem.h
@@ -30,6 +30,7 @@
 #define NM_MODEM_SIM_OPERATOR_ID "sim-operator-id"
 #define NM_MODEM_OPERATOR_CODE   "operator-code"
 #define NM_MODEM_APN             "apn"
+#define NM_MODEM_DEVICE_UID      "device-uid"
 
 /* Signals */
 #define NM_MODEM_PPP_STATS      "ppp-stats"
@@ -154,6 +155,7 @@ const char *nm_modem_get_sim_id(NMModem *modem);
 const char *nm_modem_get_sim_operator_id(NMModem *modem);
 const char *nm_modem_get_operator_code(NMModem *modem);
 const char *nm_modem_get_apn(NMModem *modem);
+const char *nm_modem_get_device_uid(NMModem *modem);
 
 gboolean nm_modem_set_data_port(NMModem        *self,
                                 NMPlatform     *platform,
diff --git a/src/core/dns/nm-dns-dnsconfd.c b/src/core/dns/nm-dns-dnsconfd.c
index 3789caf8..5f32f3fa 100644
--- a/src/core/dns/nm-dns-dnsconfd.c
+++ b/src/core/dns/nm-dns-dnsconfd.c
@@ -374,7 +374,7 @@ server_builder_append_base(GVariantBuilder   *argument_builder,
     NMDnsServer dns_server;
     gsize       addr_size;
 
-    if (!nm_dns_uri_parse(address_family, address_string, &dns_server))
+    if (!nm_dns_uri_parse(address_family, address_string, &dns_server, NULL))
         return FALSE;
     addr_size = nm_utils_addr_family_to_size(dns_server.addr_family);
 
diff --git a/src/core/dns/nm-dns-dnsmasq.c b/src/core/dns/nm-dns-dnsmasq.c
index 8e3c10c9..cc0a8788 100644
--- a/src/core/dns/nm-dns-dnsmasq.c
+++ b/src/core/dns/nm-dns-dnsmasq.c
@@ -521,9 +521,10 @@ _gl_pid_spawn_next_step(void)
     argv[argv_idx++] = "--no-resolv"; /* Use only commandline */
     argv[argv_idx++] = "--keep-in-foreground";
     argv[argv_idx++] = "--no-hosts"; /* don't use /etc/hosts to resolve */
-    argv[argv_idx++] = "--bind-interfaces";
+    argv[argv_idx++] = "--bind-dynamic";
     argv[argv_idx++] = "--pid-file=" PIDFILE;
-    argv[argv_idx++] = "--listen-address=127.0.0.1"; /* Should work for both 4 and 6 */
+    argv[argv_idx++] = "--listen-address=127.0.0.1";
+    argv[argv_idx++] = "--listen-address=::1";
     argv[argv_idx++] = "--cache-size=400";
     argv[argv_idx++] = "--clear-on-reload";     /* clear cache when dns server changes */
     argv[argv_idx++] = "--conf-file=/dev/null"; /* avoid loading /etc/dnsmasq.conf */
diff --git a/src/core/dns/nm-dns-manager.c b/src/core/dns/nm-dns-manager.c
index 57e73226..c746e714 100644
--- a/src/core/dns/nm-dns-manager.c
+++ b/src/core/dns/nm-dns-manager.c
@@ -26,6 +26,7 @@
 
 #include "libnm-core-intern/nm-core-internal.h"
 #include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 
 #include "NetworkManagerUtils.h"
 #include "devices/nm-device.h"
@@ -1006,7 +1007,8 @@ _read_link_cached(const char *path, gboolean *is_cached, char **cached)
 #define MY_RESOLV_CONF_TMP MY_RESOLV_CONF ".tmp"
 #define RESOLV_CONF_TMP    "/etc/.resolv.conf.NetworkManager"
 
-#define NO_STUB_RESOLV_CONF NMRUNDIR "/no-stub-resolv.conf"
+#define NO_STUB_RESOLV_CONF     NMRUNDIR "/no-stub-resolv.conf"
+#define NO_STUB_RESOLV_CONF_TMP NMRUNDIR "/no-stub-resolv.conf.tmp"
 
 static void
 update_resolv_conf_no_stub(NMDnsManager      *self,
@@ -1019,7 +1021,14 @@ update_resolv_conf_no_stub(NMDnsManager      *self,
 
     content = create_resolv_conf(searches, nameservers, options);
 
-    if (!g_file_set_contents(NO_STUB_RESOLV_CONF, content, -1, &local)) {
+    if (!nm_utils_file_set_contents(NO_STUB_RESOLV_CONF,
+                                    content,
+                                    -1,
+                                    0644,
+                                    NULL,
+                                    NO_STUB_RESOLV_CONF_TMP,
+                                    NULL,
+                                    &local)) {
         _LOGD("update-resolv-no-stub: failure to write file: %s", local->message);
         g_error_free(local);
         return;
diff --git a/src/core/dns/nm-dns-systemd-resolved.c b/src/core/dns/nm-dns-systemd-resolved.c
index 0701a1dc..172bd0bc 100644
--- a/src/core/dns/nm-dns-systemd-resolved.c
+++ b/src/core/dns/nm-dns-systemd-resolved.c
@@ -37,6 +37,7 @@
 static const char *const DBUS_OP_SET_LINK_DEFAULT_ROUTE = "SetLinkDefaultRoute";
 static const char *const DBUS_OP_SET_LINK_DNS_OVER_TLS  = "SetLinkDNSOverTLS";
 static const char *const DBUS_OP_SET_LINK_DNS_EX        = "SetLinkDNSEx";
+static const char *const DBUS_OP_SET_LINK_DNSSEC        = "SetLinkDNSSEC";
 
 /*****************************************************************************/
 
@@ -398,7 +399,7 @@ update_add_ip_config(NMDnsSystemdResolved    *self,
     for (i = 0; i < n; i++) {
         NMDnsServer dns_server;
 
-        if (!nm_dns_uri_parse(ip_data->addr_family, strarr[i], &dns_server))
+        if (!nm_dns_uri_parse(ip_data->addr_family, strarr[i], &dns_server, NULL))
             continue;
 
         if (!NM_IN_SET(dns_server.scheme,
@@ -484,9 +485,11 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
     NMSettingConnectionMdns       mdns              = NM_SETTING_CONNECTION_MDNS_DEFAULT;
     NMSettingConnectionLlmnr      llmnr             = NM_SETTING_CONNECTION_LLMNR_DEFAULT;
     NMSettingConnectionDnsOverTls dns_over_tls      = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT;
+    NMSettingConnectionDnssec     dnssec            = NM_SETTING_CONNECTION_DNSSEC_DEFAULT;
     const char                   *mdns_arg          = NULL;
     const char                   *llmnr_arg         = NULL;
     const char                   *dns_over_tls_arg  = NULL;
+    const char                   *dnssec_arg        = NULL;
     gboolean                      has_config        = FALSE;
     gboolean                      has_default_route = FALSE;
     guint                         i;
@@ -517,6 +520,7 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
                 llmnr = NM_MAX(llmnr, nm_l3_config_data_get_llmnr(ip_data->l3cd));
                 dns_over_tls =
                     NM_MAX(dns_over_tls, nm_l3_config_data_get_dns_over_tls(ip_data->l3cd));
+                dnssec = NM_MAX(dnssec, nm_l3_config_data_get_dnssec(ip_data->l3cd));
             }
         }
     }
@@ -589,8 +593,24 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
     }
     nm_assert(dns_over_tls_arg);
 
+    switch (dnssec) {
+    case NM_SETTING_CONNECTION_DNSSEC_NO:
+        dnssec_arg = "no";
+        break;
+    case NM_SETTING_CONNECTION_DNSSEC_ALLOW_DOWNGRADE:
+        dnssec_arg = "allow-downgrade";
+        break;
+    case NM_SETTING_CONNECTION_DNSSEC_YES:
+        dnssec_arg = "yes";
+        break;
+    case NM_SETTING_CONNECTION_DNSSEC_DEFAULT:
+        dnssec_arg = "";
+        break;
+    }
+    nm_assert(dnssec_arg);
+
     if (!nm_str_is_empty(mdns_arg) || !nm_str_is_empty(llmnr_arg)
-        || !nm_str_is_empty(dns_over_tls_arg))
+        || !nm_str_is_empty(dns_over_tls_arg) || !nm_str_is_empty(dnssec_arg))
         has_config = TRUE;
 
     _request_item_append(self, "SetLinkDomains", ic->ifindex, g_variant_builder_end(&domains));
@@ -618,6 +638,10 @@ prepare_one_interface(NMDnsSystemdResolved *self, const InterfaceConfig *ic)
                          DBUS_OP_SET_LINK_DNS_OVER_TLS,
                          ic->ifindex,
                          g_variant_new("(is)", ic->ifindex, dns_over_tls_arg ?: ""));
+    _request_item_append(self,
+                         DBUS_OP_SET_LINK_DNSSEC,
+                         ic->ifindex,
+                         g_variant_new("(is)", ic->ifindex, dnssec_arg ?: ""));
 
     return has_config;
 }
diff --git a/src/core/main-utils.c b/src/core/main-utils.c
index 0f62da29..d1be6814 100644
--- a/src/core/main-utils.c
+++ b/src/core/main-utils.c
@@ -81,7 +81,7 @@ nm_main_utils_write_pidfile(const char *pidfile)
     char                  pid[16];
 
     nm_sprintf_buf(pid, "%lld", (long long) getpid());
-    if (!nm_utils_file_set_contents(pidfile, pid, -1, 00644, NULL, NULL, &error)) {
+    if (!nm_utils_file_set_contents(pidfile, pid, -1, 00644, NULL, NULL, NULL, &error)) {
         fprintf(stderr, _("Writing to %s failed: %s\n"), pidfile, error->message);
         return FALSE;
     }
diff --git a/src/core/main.c b/src/core/main.c
index 4c7de6cd..8d519c00 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -339,7 +339,7 @@ main(int argc, char *argv[])
         char *path, *slash;
         int   g;
 
-        /* exe is <basedir>/src/.libs/lt-NetworkManager, so chop off
+        /* exe is <builddir>/src/core/NetworkManager, so chop off
          * the last three components */
         path = realpath("/proc/self/exe", NULL);
         g_assert(path != NULL);
diff --git a/src/core/ndisc/nm-lndp-ndisc.c b/src/core/ndisc/nm-lndp-ndisc.c
index f0de2fd5..c19dcc91 100644
--- a/src/core/ndisc/nm-lndp-ndisc.c
+++ b/src/core/ndisc/nm-lndp-ndisc.c
@@ -19,6 +19,7 @@
 #include "libnm-systemd-shared/nm-sd-utils-shared.h"
 #include "nm-l3cfg.h"
 #include "nm-ndisc-private.h"
+#include "nm-core-utils.h"
 
 #define _NMLOG_PREFIX_NAME "ndisc-lndp"
 
@@ -27,6 +28,14 @@
 typedef struct {
     struct ndp *ndp;
     GSource    *event_source;
+
+    struct {
+        NMRateLimit pio_lft;
+        NMRateLimit mtu;
+        NMRateLimit omit_prefix;
+        NMRateLimit omit_dns;
+        NMRateLimit omit_dnssl;
+    } msg_ratelimit;
 } NMLndpNDiscPrivate;
 
 /*****************************************************************************/
@@ -49,6 +58,36 @@ G_DEFINE_TYPE(NMLndpNDisc, nm_lndp_ndisc, NM_TYPE_NDISC)
 
 /*****************************************************************************/
 
+/*
+ * If we log a message about an invalid RA packet, don't repeat the same message
+ * at every packet received or sent. Rate limit the message to 6 every 12 hours
+ * per type and per ndisc instance.
+ */
+
+#define LOG_INV_RA_WINDOW (12 * 3600)
+#define LOG_INV_RA_BURST  6
+
+#define _LOG_INVALID_RA(ndisc, rate_limit, ...)                                  \
+    G_STMT_START                                                                 \
+    {                                                                            \
+        NMNDisc     *__ndisc  = (ndisc);                                         \
+        NMRateLimit *__rl     = (rate_limit);                                    \
+        const char  *__ifname = nm_ndisc_get_ifname(__ndisc);                    \
+                                                                                 \
+        if (__ifname && nm_logging_enabled(LOGL_WARN, LOGD_IP6)                  \
+            && nm_rate_limit_check(__rl, LOG_INV_RA_WINDOW, LOG_INV_RA_BURST)) { \
+            nm_log(LOGL_WARN,                                                    \
+                   LOGD_IP6,                                                     \
+                   __ifname,                                                     \
+                   NULL,                                                         \
+                   "ndisc (%s): " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),            \
+                   __ifname _NM_UTILS_MACRO_REST(__VA_ARGS__));                  \
+        }                                                                        \
+    }                                                                            \
+    G_STMT_END
+
+/*****************************************************************************/
+
 static gboolean
 send_rs(NMNDisc *ndisc, GError **error)
 {
@@ -113,6 +152,7 @@ static int
 receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
 {
     NMNDisc             *ndisc   = (NMNDisc *) user_data;
+    NMLndpNDiscPrivate  *priv    = NM_LNDP_NDISC_GET_PRIVATE(ndisc);
     NMNDiscDataInternal *rdata   = ndisc->rdata;
     NMNDiscConfigMap     changed = 0;
     NMNDiscGateway       gateway;
@@ -229,7 +269,11 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
              * log a system management error in this case.
              */
             if (preferred_time > valid_time) {
-                _LOGW("skipping PIO - preferred lifetime > valid lifetime");
+                _LOG_INVALID_RA(
+                    ndisc,
+                    &priv->msg_ratelimit.pio_lft,
+                    "ignoring Prefix Information Option with invalid lifetimes in received IPv6 "
+                    "router advertisement");
                 continue;
             }
 
@@ -349,7 +393,11 @@ receive_ra(struct ndp *ndp, struct ndp_msg *msg, gpointer user_data)
              * Kernel would set it, but would flush out all IPv6 addresses away
              * from the link, even the link-local, and we wouldn't be able to
              * listen for further RAs that could fix the MTU. */
-            _LOGW("MTU too small for IPv6 ignored: %d", mtu);
+            _LOG_INVALID_RA(ndisc,
+                            &priv->msg_ratelimit.mtu,
+                            "ignoring too small MTU %u in received IPv6 "
+                            "router advertisement",
+                            mtu);
         }
     }
 
@@ -445,8 +493,11 @@ send_ra(NMNDisc *ndisc, GError **error)
 
         prefix = _ndp_msg_add_option(msg, sizeof(*prefix));
         if (!prefix) {
-            /* Maybe we could sent separate RAs, but why bother... */
-            _LOGW("The RA is too big, had to omit some some prefixes.");
+            /* Maybe we could send separate RAs, but why bother... */
+            _LOG_INVALID_RA(
+                ndisc,
+                &priv->msg_ratelimit.omit_prefix,
+                "the outgoing IPv6 router advertisement is too big: omitting some prefixes");
             break;
         }
 
@@ -475,7 +526,10 @@ send_ra(NMNDisc *ndisc, GError **error)
 
         option = _ndp_msg_add_option(msg, len);
         if (!option) {
-            _LOGW("The RA is too big, had to omit DNS information.");
+            _LOG_INVALID_RA(
+                ndisc,
+                &priv->msg_ratelimit.omit_dns,
+                "the outgoing IPv6 router advertisement is too big: omitting DNS information");
             goto dns_servers_done;
         }
 
@@ -553,7 +607,10 @@ dns_servers_done:
         nm_assert(len / 8u >= 2u);
 
         if (len / 8u >= 256u || !(option = _ndp_msg_add_option(msg, len))) {
-            _LOGW("The RA is too big, had to omit DNS search list.");
+            _LOG_INVALID_RA(
+                ndisc,
+                &priv->msg_ratelimit.omit_dnssl,
+                "the outgoing IPv6 router advertisement is too big: omitting DNS search list");
             goto dns_domains_done;
         }
 
diff --git a/src/core/nm-connectivity.c b/src/core/nm-connectivity.c
index 2aa22331..c7915fd9 100644
--- a/src/core/nm-connectivity.c
+++ b/src/core/nm-connectivity.c
@@ -77,6 +77,8 @@ struct _NMConnectivityCheckHandle {
         ConConfig *con_config;
 
         GCancellable      *resolve_cancellable;
+        int                resolve_ifindex;
+        GDBusConnection   *dbus_connection;
         CURLM             *curl_mhandle;
         CURL              *curl_ehandle;
         struct curl_slist *request_headers;
@@ -953,6 +955,113 @@ systemd_resolved_resolve_cb(GObject *object, GAsyncResult *res, gpointer user_da
     do_curl_request(cb_data, nm_str_buf_get_str(&strbuf_hosts));
 }
 
+static void
+systemd_resolved_resolve(NMConnectivityCheckHandle *cb_data)
+{
+    _LOG2D("start request to '%s' (try resolving '%s' using systemd-resolved with ifindex %d)",
+           cb_data->concheck.con_config->uri,
+           cb_data->concheck.con_config->host,
+           cb_data->concheck.resolve_ifindex);
+
+    g_dbus_connection_call(cb_data->concheck.dbus_connection,
+                           "org.freedesktop.resolve1",
+                           "/org/freedesktop/resolve1",
+                           "org.freedesktop.resolve1.Manager",
+                           "ResolveHostname",
+                           g_variant_new("(isit)",
+                                         (gint32) cb_data->concheck.resolve_ifindex,
+                                         cb_data->concheck.con_config->host,
+                                         (gint32) cb_data->addr_family,
+                                         SD_RESOLVED_DNS),
+                           G_VARIANT_TYPE("(a(iiay)st)"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           cb_data->concheck.resolve_cancellable,
+                           systemd_resolved_resolve_cb,
+                           cb_data);
+}
+
+static void
+systemd_resolved_link_scopes_cb(GObject *object, GAsyncResult *res, gpointer user_data)
+{
+    NMConnectivityCheckHandle *cb_data;
+    gs_unref_variant GVariant *result     = NULL;
+    gs_unref_variant GVariant *value      = NULL;
+    gs_free_error GError      *error      = NULL;
+    guint64                    scope_mask = 0;
+
+    result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), res, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    cb_data = user_data;
+
+    if (!result) {
+        _LOG2D("unable to obtain systemd-resolved link ScopesMask for interface %d: %s",
+               cb_data->concheck.resolve_ifindex,
+               error->message);
+
+        cb_data->concheck.resolve_ifindex = 0;
+        systemd_resolved_resolve(cb_data);
+        return;
+    }
+
+    g_variant_get(result, "(v)", &value);
+    g_variant_get(value, "t", &scope_mask);
+
+    if (!(scope_mask & SD_RESOLVED_DNS)) {
+        /* there is no per-link DNS configured / active; query all available /
+         * system DNS resolvers instead of restricting the lookup to just this
+         * one, which would turn up no results. */
+        _LOG2D("no per-link DNS available (scope mask %" G_GUINT64_FORMAT
+               "); falling back to system-wide lookups",
+               scope_mask);
+        cb_data->concheck.resolve_ifindex = 0;
+    }
+
+    systemd_resolved_resolve(cb_data);
+}
+
+static void
+systemd_resolved_get_link_cb(GObject *object, GAsyncResult *res, gpointer user_data)
+{
+    NMConnectivityCheckHandle *cb_data;
+    gs_unref_variant GVariant *result    = NULL;
+    gs_free char              *link_path = NULL;
+    gs_free_error GError      *error     = NULL;
+
+    result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), res, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    cb_data = user_data;
+
+    if (!result) {
+        _LOG2D("unable to obtain systemd-resolved link D-Bus object for interface %d: %s",
+               cb_data->concheck.resolve_ifindex,
+               error->message);
+
+        cb_data->concheck.resolve_ifindex = 0;
+        systemd_resolved_resolve(cb_data);
+        return;
+    }
+
+    g_variant_get(result, "(o)", &link_path);
+
+    g_dbus_connection_call(cb_data->concheck.dbus_connection,
+                           "org.freedesktop.resolve1",
+                           link_path,
+                           "org.freedesktop.DBus.Properties",
+                           "Get",
+                           g_variant_new("(ss)", "org.freedesktop.resolve1.Link", "ScopesMask"),
+                           G_VARIANT_TYPE("(v)"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           -1,
+                           cb_data->concheck.resolve_cancellable,
+                           systemd_resolved_link_scopes_cb,
+                           cb_data);
+}
+
 static NMConnectivityState
 check_platform_config(NMConnectivity *self,
                       NMPlatform     *platform,
@@ -1067,6 +1176,7 @@ nm_connectivity_check_start(NMConnectivity             *self,
         }
 
         cb_data->concheck.resolve_cancellable = g_cancellable_new();
+        cb_data->concheck.resolve_ifindex     = ifindex;
 
         /* note that we pick up support for systemd-resolved right away when we need it.
          * We don't need to remember the setting, because we can (cheaply) check anew
@@ -1089,10 +1199,8 @@ nm_connectivity_check_start(NMConnectivity             *self,
         has_systemd_resolved = !!nm_dns_manager_get_systemd_resolved(nm_dns_manager_get());
 
         if (has_systemd_resolved) {
-            GDBusConnection *dbus_connection;
-
-            dbus_connection = NM_MAIN_DBUS_CONNECTION_GET;
-            if (!dbus_connection) {
+            cb_data->concheck.dbus_connection = NM_MAIN_DBUS_CONNECTION_GET;
+            if (!cb_data->concheck.dbus_connection) {
                 /* we have no D-Bus connection? That might happen in configure and quit mode.
                  *
                  * Anyway, something is very odd, just fail connectivity check. */
@@ -1103,25 +1211,19 @@ nm_connectivity_check_start(NMConnectivity             *self,
                 return cb_data;
             }
 
-            g_dbus_connection_call(dbus_connection,
+            /* first check whether there has been a per-link DNS configured */
+            g_dbus_connection_call(cb_data->concheck.dbus_connection,
                                    "org.freedesktop.resolve1",
                                    "/org/freedesktop/resolve1",
                                    "org.freedesktop.resolve1.Manager",
-                                   "ResolveHostname",
-                                   g_variant_new("(isit)",
-                                                 0,
-                                                 cb_data->concheck.con_config->host,
-                                                 (gint32) cb_data->addr_family,
-                                                 SD_RESOLVED_DNS),
-                                   G_VARIANT_TYPE("(a(iiay)st)"),
+                                   "GetLink",
+                                   g_variant_new("(i)", ifindex),
+                                   G_VARIANT_TYPE("(o)"),
                                    G_DBUS_CALL_FLAGS_NONE,
                                    -1,
                                    cb_data->concheck.resolve_cancellable,
-                                   systemd_resolved_resolve_cb,
+                                   systemd_resolved_get_link_cb,
                                    cb_data);
-            _LOG2D("start request to '%s' (try resolving '%s' using systemd-resolved)",
-                   cb_data->concheck.con_config->uri,
-                   cb_data->concheck.con_config->host);
             return cb_data;
         }
 
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 4a7f760d..deac04e7 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -2865,6 +2865,7 @@ _host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
                                                0600,
                                                NULL,
                                                NULL,
+                                               NULL,
                                                &error)) {
             nm_log_warn(
                 LOGD_CORE,
@@ -5162,6 +5163,14 @@ helper_have_data(int fd, GIOCondition condition, gpointer user_data)
     n_read = nm_utils_fd_read(fd, &info->in_buffer);
     _LOG2T(info, "read returns %ld", (long) n_read);
 
+    if (info->in_buffer.len > 32 * 1024 * 1024) {
+        helper_complete(info,
+                        g_error_new_literal(NM_UTILS_ERROR,
+                                            NM_UTILS_ERROR_UNKNOWN,
+                                            "the output is larger than 32MiB"));
+        return G_SOURCE_CONTINUE;
+    }
+
     if (n_read > 0)
         return G_SOURCE_CONTINUE;
 
@@ -5504,6 +5513,155 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
     return TRUE;
 }
 
+/**
+ * nm_utils_connection_supported:
+ * @connection: the connection
+ * @error: on return, the reason why the connection in not supported
+ *
+ * Returns whether the given connection is supported by this version
+ * of NetworkManager.
+ */
+gboolean
+nm_utils_connection_supported(NMConnection *connection, GError **error)
+{
+    const char *type;
+    const char *feature = NULL;
+
+    g_return_val_if_fail(connection, FALSE);
+    g_return_val_if_fail(!error || !*error, FALSE);
+
+    type = nm_connection_get_connection_type(connection);
+
+    if (!WITH_TEAMDCTL) {
+        NMSettingConnection *s_con;
+
+        if (nm_streq0(type, NM_SETTING_TEAM_SETTING_NAME)) {
+            feature = "team";
+            goto out_disabled;
+        }
+
+        /* Match team ports */
+        if ((s_con = nm_connection_get_setting_connection(connection))
+            && nm_streq0(nm_setting_connection_get_port_type(s_con),
+                         NM_SETTING_TEAM_SETTING_NAME)) {
+            feature = "team";
+            goto out_disabled;
+        }
+    }
+
+    if (!WITH_OPENVSWITCH) {
+        if (NM_IN_STRSET(type,
+                         NM_SETTING_OVS_BRIDGE_SETTING_NAME,
+                         NM_SETTING_OVS_PORT_SETTING_NAME,
+                         NM_SETTING_OVS_INTERFACE_SETTING_NAME)) {
+            feature = "Open vSwitch";
+            goto out_disabled;
+        }
+
+        /* Match OVS system interfaces */
+        if (nm_connection_get_setting_ovs_interface(connection)) {
+            feature = "Open vSwitch";
+            goto out_disabled;
+        }
+    }
+
+    if (!WITH_WIFI
+        && NM_IN_STRSET(type,
+                        NM_SETTING_WIRELESS_SETTING_NAME,
+                        NM_SETTING_OLPC_MESH_SETTING_NAME,
+                        NM_SETTING_WIFI_P2P_SETTING_NAME)) {
+        feature = "Wi-Fi";
+        goto out_disabled;
+    }
+
+    if (!WITH_WWAN
+        && NM_IN_STRSET(type, NM_SETTING_GSM_SETTING_NAME, NM_SETTING_CDMA_SETTING_NAME)) {
+        feature = "WWAN";
+        goto out_disabled;
+    }
+
+    if (nm_streq0(type, NM_SETTING_WIMAX_SETTING_NAME)) {
+        feature = "WiMAX";
+        goto out_removed;
+    }
+
+    return TRUE;
+
+out_disabled:
+    nm_assert(feature);
+    g_set_error(error,
+                NM_SETTINGS_ERROR,
+                NM_SETTINGS_ERROR_FEATURE_DISABLED,
+                "%s support is disabled in this build",
+                feature);
+    return FALSE;
+
+out_removed:
+    nm_assert(feature);
+    g_set_error(error,
+                NM_SETTINGS_ERROR,
+                NM_SETTINGS_ERROR_FEATURE_REMOVED,
+                "%s is no longer supported",
+                feature);
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_rate_limit_check():
+ * @rate_limit: the NMRateLimit instance
+ * @window_sec: the time window in seconds, between 1 and 864000 (ten days)
+ * @burst: the number of max allowed event occurrences in the given time
+ *   window
+ *
+ * The function rate limits an event. Call it multiple times with the
+ * same @window_sec, and @burst values.
+ *
+ * Returns: TRUE if the event is allowed, FALSE if it is rate-limited
+ */
+gboolean
+nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 burst)
+{
+    gint64 now;
+    gint64 old_ts_msec;
+    gint64 window_msec;
+    gint64 capacity;
+    gint64 elapsed;
+
+    nm_assert(window_sec >= 1 && window_sec <= 864000);
+    nm_assert(burst >= 1);
+
+    /* This implements a simple token bucket algorithm. For each millisecond,
+     * refill "burst" tokens. Thus, during a full time window we
+     * refill (window_msec * burst) tokens. Each event consumes @window_msec
+     * tokens. */
+
+    window_msec         = (gint64) window_sec * NM_UTILS_MSEC_PER_SEC;
+    capacity            = window_msec * (gint64) burst;
+    old_ts_msec         = rate_limit->ts_msec;
+    now                 = nm_utils_get_monotonic_timestamp_msec();
+    rate_limit->ts_msec = now;
+
+    elapsed = now - old_ts_msec;
+    if (old_ts_msec == 0 || elapsed > window_msec) {
+        /* On the first call, or in case a whole window passed, (re)start with
+         * a full budget */
+        rate_limit->tokens = capacity;
+    } else {
+        rate_limit->tokens += elapsed * (gint64) burst;
+        rate_limit->tokens = NM_MIN(rate_limit->tokens, capacity);
+    }
+
+    /* Consume the tokens */
+    if (rate_limit->tokens >= window_msec) {
+        rate_limit->tokens -= window_msec;
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
 const char *
 nm_utils_get_connection_first_permissions_user(NMConnection *connection)
 {
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index 224018c6..cccccae6 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -494,6 +494,19 @@ gid_t nm_utils_get_nm_gid(void);
 
 /*****************************************************************************/
 
+gboolean nm_utils_connection_supported(NMConnection *connection, GError **error);
+
+/*****************************************************************************/
+
+typedef struct {
+    gint64 ts_msec;
+    gint64 tokens;
+} NMRateLimit;
+
+gboolean nm_rate_limit_check(NMRateLimit *rate_limit, gint32 window_sec, gint32 burst);
+
+/*****************************************************************************/
+
 const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection);
 
 /*****************************************************************************/
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 666aa8a3..328f59b6 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -120,6 +120,7 @@ struct _NML3ConfigData {
     NMSettingConnectionMdns       mdns;
     NMSettingConnectionLlmnr      llmnr;
     NMSettingConnectionDnsOverTls dns_over_tls;
+    NMSettingConnectionDnssec     dnssec;
     NMUtilsIPv6IfaceId            ip6_token;
 
     NML3ConfigDatFlags flags;
@@ -577,6 +578,16 @@ nm_l3_config_data_log(const NML3ConfigData *self,
                                            NULL)));
     }
 
+    if (self->dnssec != NM_SETTING_CONNECTION_DNSSEC_DEFAULT) {
+        gs_free char *s = NULL;
+
+        _L("dnssec: %s",
+           (s = _nm_utils_enum_to_str_full(nm_setting_connection_dnssec_get_type(),
+                                           self->dnssec,
+                                           " ",
+                                           NULL)));
+    }
+
     if (self->mptcp_flags != NM_MPTCP_FLAGS_NONE) {
         gs_free char *s = NULL;
 
@@ -694,6 +705,7 @@ nm_l3_config_data_new(NMDedupMultiIndex *multi_idx, int ifindex, NMIPConfigSourc
         .mdns                           = NM_SETTING_CONNECTION_MDNS_DEFAULT,
         .llmnr                          = NM_SETTING_CONNECTION_LLMNR_DEFAULT,
         .dns_over_tls                   = NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT,
+        .dnssec                         = NM_SETTING_CONNECTION_DNSSEC_DEFAULT,
         .flags                          = NM_L3_CONFIG_DAT_FLAGS_NONE,
         .metered                        = NM_TERNARY_DEFAULT,
         .proxy_browser_only             = NM_TERNARY_DEFAULT,
@@ -1767,6 +1779,26 @@ nm_l3_config_data_set_dns_over_tls(NML3ConfigData *self, NMSettingConnectionDnsO
     return TRUE;
 }
 
+NMSettingConnectionDnssec
+nm_l3_config_data_get_dnssec(const NML3ConfigData *self)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, TRUE));
+
+    return self->dnssec;
+}
+
+gboolean
+nm_l3_config_data_set_dnssec(NML3ConfigData *self, NMSettingConnectionDnssec dnssec)
+{
+    nm_assert(_NM_IS_L3_CONFIG_DATA(self, FALSE));
+
+    if (self->dnssec == dnssec)
+        return FALSE;
+
+    self->dnssec = dnssec;
+    return TRUE;
+}
+
 NMIPRouteTableSyncMode
 nm_l3_config_data_get_route_table_sync(const NML3ConfigData *self, int addr_family)
 {
@@ -2446,6 +2478,7 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
         NM_CMP_DIRECT(a->mdns, b->mdns);
         NM_CMP_DIRECT(a->llmnr, b->llmnr);
         NM_CMP_DIRECT(a->dns_over_tls, b->dns_over_tls);
+        NM_CMP_DIRECT(a->dnssec, b->dnssec);
     }
 
     if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_OTHER)) {
@@ -3211,6 +3244,12 @@ nm_l3_config_data_hash_dns(const NML3ConfigData *l3cd,
         empty = FALSE;
     }
 
+    val = nm_l3_config_data_get_dnssec(l3cd);
+    if (val != NM_SETTING_CONNECTION_DNSSEC_DEFAULT) {
+        g_checksum_update(sum, (const guint8 *) &val, sizeof(val));
+        empty = FALSE;
+    }
+
     if (!empty) {
         int prio = 0;
 
@@ -3461,6 +3500,9 @@ nm_l3_config_data_merge(NML3ConfigData       *self,
     if (self->dns_over_tls == NM_SETTING_CONNECTION_DNS_OVER_TLS_DEFAULT)
         self->dns_over_tls = src->dns_over_tls;
 
+    if (self->dnssec == NM_SETTING_CONNECTION_DNSSEC_DEFAULT)
+        self->dnssec = src->dnssec;
+
     if (self->ip6_token.id == 0)
         self->ip6_token.id = src->ip6_token.id;
 
diff --git a/src/core/nm-l3-config-data.h b/src/core/nm-l3-config-data.h
index 265e126d..4102b6e1 100644
--- a/src/core/nm-l3-config-data.h
+++ b/src/core/nm-l3-config-data.h
@@ -458,6 +458,10 @@ NMSettingConnectionDnsOverTls nm_l3_config_data_get_dns_over_tls(const NML3Confi
 gboolean nm_l3_config_data_set_dns_over_tls(NML3ConfigData               *self,
                                             NMSettingConnectionDnsOverTls dns_over_tls);
 
+NMSettingConnectionDnssec nm_l3_config_data_get_dnssec(const NML3ConfigData *self);
+
+gboolean nm_l3_config_data_set_dnssec(NML3ConfigData *self, NMSettingConnectionDnssec dnssec);
+
 NMIPRouteTableSyncMode nm_l3_config_data_get_route_table_sync(const NML3ConfigData *self,
                                                               int                   addr_family);
 
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 1797cd0f..0d93f76b 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -40,8 +40,7 @@ G_STATIC_ASSERT(NM_ACD_TIMEOUT_RFC5227_MSEC == N_ACD_TIMEOUT_RFC5227);
 
 #define ACD_SUPPORTED_ETH_ALEN                  ETH_ALEN
 #define ACD_ENSURE_RATELIMIT_MSEC               ((guint32) 4000u)
-#define ACD_WAIT_PROBING_EXTRA_TIME_MSEC        ((guint32) (1000u + ACD_ENSURE_RATELIMIT_MSEC))
-#define ACD_WAIT_PROBING_EXTRA_TIME2_MSEC       ((guint32) 1000u)
+#define ACD_WAIT_PROBING_EXTRA_TIME_MSEC        ((guint32) (2000u + ACD_ENSURE_RATELIMIT_MSEC))
 #define ACD_WAIT_TIME_PROBING_FULL_RESTART_MSEC ((guint32) 30000u)
 #define ACD_WAIT_TIME_CONFLICT_RESTART_MSEC     ((guint32) 120000u)
 #define ACD_WAIT_TIME_ANNOUNCE_RESTART_MSEC     ((guint32) 30000u)
@@ -2740,9 +2739,8 @@ handle_init:
             nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
 
             if (acd_data->info.state == NM_L3_ACD_ADDR_STATE_PROBING) {
-                if ((*p_now_msec) > acd_data->probing_timestamp_msec
-                                        + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
-                                        + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC) {
+                if ((*p_now_msec)
+                    > acd_data->probing_timestamp_msec + ACD_WAIT_PROBING_EXTRA_TIME_MSEC) {
                     /* hm. We failed to create a new probe too long. Something is really wrong
                      * internally, but let's ignore the issue and assume the address is good. What
                      * else would we do? Assume the address is USED? */
@@ -2948,7 +2946,7 @@ handle_init:
             nm_utils_get_monotonic_timestamp_msec_cached(p_now_msec);
 
             if (acd_data->probing_timestamp_msec + acd_data->probing_timeout_msec
-                    + ACD_WAIT_PROBING_EXTRA_TIME_MSEC + ACD_WAIT_PROBING_EXTRA_TIME2_MSEC
+                    + ACD_WAIT_PROBING_EXTRA_TIME_MSEC
                 >= (*p_now_msec)) {
                 /* The probing already started quite a while ago. We ignore the link event
                  * and let the probe come to it's natural end. */
@@ -3058,9 +3056,10 @@ handle_start_probing:
         }
 
         _LOGT_acd(acd_data,
-                  "%sstart probing (timeout %u msec, %s)",
+                  "%sstart probing (timeout %u msec, ebpf %s; %s)",
                   orig_state == NM_L3_ACD_ADDR_STATE_INIT ? "" : "re",
                   acd_data->probing_timeout_msec,
+                  n_acd_has_bpf(self->priv.p->nacd) ? "enabled" : "disabled",
                   log_reason);
         return;
     }
@@ -3155,10 +3154,11 @@ handle_start_defending:
         }
 
         _LOGT_acd(acd_data,
-                  "start announcing (defend=%s) (probe created)",
+                  "start announcing (defend=%s) (probe created with ebpf %s)",
                   _l3_acd_defend_type_to_string(acd_data->acd_defend_type_current,
                                                 sbuf256,
-                                                sizeof(sbuf256)));
+                                                sizeof(sbuf256)),
+                  n_acd_has_bpf(self->priv.p->nacd) ? "enabled" : "disabled");
         acd_data->acd_defend_type_is_active = FALSE;
         acd_data->nacd_probe                = probe;
         return;
@@ -3989,7 +3989,7 @@ _l3cfg_routed_dns_apply(NML3Cfg *self, const NML3ConfigData *l3cd)
             NMDnsServer               dns;
             int                       r;
 
-            if (!nm_dns_uri_parse(addr_family, nameservers[i], &dns))
+            if (!nm_dns_uri_parse(addr_family, nameservers[i], &dns, NULL))
                 continue;
 
             /* Find the gateway to the DNS over the current interface. When
@@ -5054,8 +5054,8 @@ _l3_commit_mptcp_af(NML3Cfg          *self,
             (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SIGNAL) ? MPTCP_PM_ADDR_FLAG_SIGNAL : 0)
             | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_SUBFLOW) ? MPTCP_PM_ADDR_FLAG_SUBFLOW : 0)
             | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_BACKUP) ? MPTCP_PM_ADDR_FLAG_BACKUP : 0)
-            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_FULLMESH) ? MPTCP_PM_ADDR_FLAG_FULLMESH
-                                                                  : 0);
+            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_FULLMESH) ? MPTCP_PM_ADDR_FLAG_FULLMESH : 0)
+            | (NM_FLAGS_HAS(mptcp_flags, NM_MPTCP_FLAGS_LAMINAR) ? MPTCP_PM_ADDR_FLAG_LAMINAR : 0);
         NMPlatformMptcpAddr a = {
             .ifindex     = self->priv.ifindex,
             .id          = 0,
diff --git a/src/core/nm-netns.c b/src/core/nm-netns.c
index f481bc36..f55d1132 100644
--- a/src/core/nm-netns.c
+++ b/src/core/nm-netns.c
@@ -68,7 +68,7 @@ typedef struct {
     NMPNetns         *platform_netns;
     NMPGlobalTracker *global_tracker;
     GHashTable       *l3cfgs;
-    GHashTable       *shared_ips;
+    GHashTable       *ip_reservation[_NM_NETNS_IP_RESERVATION_TYPE_NUM];
     GHashTable       *ecmp_track_by_obj;
     GHashTable       *ecmp_track_by_ecmpid;
 
@@ -571,106 +571,150 @@ notify_watcher:
 
 /*****************************************************************************/
 
-NMNetnsSharedIPHandle *
-nm_netns_shared_ip_reserve(NMNetns *self)
-{
-    NMNetnsPrivate        *priv;
-    NMNetnsSharedIPHandle *handle;
-    const in_addr_t        addr_start = ntohl(0x0a2a0001u); /* 10.42.0.1 */
-    in_addr_t              addr;
-    char                   sbuf_addr[NM_INET_ADDRSTRLEN];
+typedef struct {
+    const char *name;
+    guint32     start_addr; /* host byte order */
+    guint       prefix_len;
+    guint       num_addrs;
+    gboolean    allow_reuse;
+} IPReservationTypeDesc;
+
+static const IPReservationTypeDesc ip_reservation_types[_NM_NETNS_IP_RESERVATION_TYPE_NUM] = {
+    [NM_NETNS_IP_RESERVATION_TYPE_SHARED4] =
+        {
+            .name        = "shared-ip4",
+            .start_addr  = 0x0a2a0001, /* 10.42.0.1 */
+            .prefix_len  = 24,
+            .num_addrs   = 256,
+            .allow_reuse = TRUE,
+        },
+};
 
-    /* Find an unused address in the 10.42.x.x range */
+NMNetnsIPReservation *
+nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type)
+{
+    NMNetnsPrivate              *priv;
+    const IPReservationTypeDesc *desc;
+    NMNetnsIPReservation        *res;
+    GHashTable                 **table;
+    in_addr_t                    addr;
+    char                         buf[NM_INET_ADDRSTRLEN];
 
     g_return_val_if_fail(NM_IS_NETNS(self), NULL);
+    g_return_val_if_fail(type < _NM_NETNS_IP_RESERVATION_TYPE_NUM, NULL);
 
-    priv = NM_NETNS_GET_PRIVATE(self);
+    priv  = NM_NETNS_GET_PRIVATE(self);
+    desc  = &ip_reservation_types[type];
+    table = &priv->ip_reservation[type];
 
-    if (!priv->shared_ips) {
-        addr             = addr_start;
-        priv->shared_ips = g_hash_table_new(nm_puint32_hash, nm_puint32_equal);
+    if (!*table) {
+        addr   = htonl(desc->start_addr);
+        *table = g_hash_table_new(nm_puint32_hash, nm_puint32_equal);
         g_object_ref(self);
     } else {
         guint32 count;
 
-        nm_assert(g_hash_table_size(priv->shared_ips) > 0);
+        nm_assert(g_hash_table_size(*table) > 0);
+        nm_assert(desc->prefix_len > 0 && desc->prefix_len <= 32);
 
         count = 0u;
         for (;;) {
-            addr = addr_start + htonl(count << 8u);
+            addr = htonl(desc->start_addr + (count << (32 - desc->prefix_len)));
 
-            handle = g_hash_table_lookup(priv->shared_ips, &addr);
-            if (!handle)
+            res = g_hash_table_lookup(*table, &addr);
+            if (!res)
                 break;
 
             count++;
 
-            if (count > 0xFFu) {
-                if (handle->_ref_count == 1) {
-                    _LOGE("shared-ip4: ran out of shared IP addresses. Reuse %s/24",
-                          nm_inet4_ntop(handle->addr, sbuf_addr));
+            if (count >= desc->num_addrs) {
+                if (!desc->allow_reuse) {
+                    _LOGE("%s: ran out of IP addresses", desc->name);
+                    return NULL;
+                }
+
+                if (res->_ref_count == 1) {
+                    _LOGE("%s: ran out of IP addresses. Reuse %s/%u",
+                          desc->name,
+                          nm_inet4_ntop(res->addr, buf),
+                          desc->prefix_len);
                 } else {
-                    _LOGD("shared-ip4: reserved IP address range %s/24 (duplicate)",
-                          nm_inet4_ntop(handle->addr, sbuf_addr));
+                    _LOGD("%s: reserved IP address %s/%u (duplicate)",
+                          desc->name,
+                          nm_inet4_ntop(res->addr, buf),
+                          desc->prefix_len);
                 }
-                handle->_ref_count++;
-                return handle;
+                res->_ref_count++;
+                return res;
             }
         }
     }
 
-    handle  = g_slice_new(NMNetnsSharedIPHandle);
-    *handle = (NMNetnsSharedIPHandle) {
+    res  = g_slice_new(NMNetnsIPReservation);
+    *res = (NMNetnsIPReservation) {
         .addr       = addr,
         ._ref_count = 1,
         ._self      = self,
+        ._type      = type,
     };
 
-    g_hash_table_add(priv->shared_ips, handle);
+    g_hash_table_add(*table, res);
 
-    _LOGD("shared-ip4: reserved IP address range %s/24", nm_inet4_ntop(handle->addr, sbuf_addr));
-    return handle;
+    _LOGD("%s: reserved IP address %s/%u",
+          desc->name,
+          nm_inet4_ntop(res->addr, buf),
+          desc->prefix_len);
+    return res;
 }
 
 void
-nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle)
+nm_netns_ip_reservation_release(NMNetnsIPReservation *res)
 {
-    NMNetns        *self;
-    NMNetnsPrivate *priv;
-    char            sbuf_addr[NM_INET_ADDRSTRLEN];
-
-    g_return_if_fail(handle);
+    NMNetns                     *self;
+    NMNetnsPrivate              *priv;
+    const IPReservationTypeDesc *desc;
+    GHashTable                 **table;
+    char                         buf[NM_INET_ADDRSTRLEN];
 
-    self = handle->_self;
+    g_return_if_fail(res);
+    g_return_if_fail(res->_type < _NM_NETNS_IP_RESERVATION_TYPE_NUM);
 
+    self = res->_self;
     g_return_if_fail(NM_IS_NETNS(self));
 
-    priv = NM_NETNS_GET_PRIVATE(self);
-
-    nm_assert(handle->_ref_count > 0);
-    nm_assert(handle == nm_g_hash_table_lookup(priv->shared_ips, handle));
-
-    if (handle->_ref_count > 1) {
-        nm_assert(handle->addr == ntohl(0x0A2AFF01u)); /* 10.42.255.1 */
-        handle->_ref_count--;
-        _LOGD("shared-ip4: release IP address range %s/24 (%d more references held)",
-              nm_inet4_ntop(handle->addr, sbuf_addr),
-              handle->_ref_count);
+    priv  = NM_NETNS_GET_PRIVATE(self);
+    desc  = &ip_reservation_types[res->_type];
+    table = &priv->ip_reservation[res->_type];
+
+    nm_assert(res->_ref_count > 0);
+    nm_assert(res == nm_g_hash_table_lookup(*table, res));
+
+    if (res->_ref_count > 1) {
+        nm_assert(desc->allow_reuse);
+        res->_ref_count--;
+        _LOGD("%s: release IP address reservation %s/%u (%d more references held)",
+              desc->name,
+              nm_inet4_ntop(res->addr, buf),
+              desc->prefix_len,
+              res->_ref_count);
         return;
     }
 
-    if (!g_hash_table_remove(priv->shared_ips, handle))
+    if (!g_hash_table_remove(*table, res))
         nm_assert_not_reached();
 
-    if (g_hash_table_size(priv->shared_ips) == 0) {
-        nm_clear_pointer(&priv->shared_ips, g_hash_table_unref);
+    _LOGD("%s: release IP address reservation %s/%u",
+          desc->name,
+          nm_inet4_ntop(res->addr, buf),
+          desc->prefix_len);
+
+    if (g_hash_table_size(*table) == 0) {
+        nm_clear_pointer(table, g_hash_table_unref);
         g_object_unref(self);
     }
 
-    _LOGD("shared-ip4: release IP address range %s/24", nm_inet4_ntop(handle->addr, sbuf_addr));
-
-    handle->_self = NULL;
-    nm_g_slice_free(handle);
+    res->_self = NULL;
+    nm_g_slice_free(res);
 }
 
 /*****************************************************************************/
@@ -1560,11 +1604,14 @@ dispose(GObject *object)
 
     nm_assert(nm_g_hash_table_size(priv->l3cfgs) == 0);
     nm_assert(c_list_is_empty(&priv->l3cfg_signal_pending_lst_head));
-    nm_assert(!priv->shared_ips);
     nm_assert(nm_g_hash_table_size(priv->watcher_idx) == 0);
     nm_assert(nm_g_hash_table_size(priv->watcher_by_tag_idx) == 0);
     nm_assert(nm_g_hash_table_size(priv->watcher_ip_data_idx) == 0);
 
+    for (guint i = 0; i < _NM_NETNS_IP_RESERVATION_TYPE_NUM; i++) {
+        nm_assert(!priv->ip_reservation[i]);
+    }
+
     nm_clear_pointer(&priv->ecmp_track_by_obj, g_hash_table_destroy);
     nm_clear_pointer(&priv->ecmp_track_by_ecmpid, g_hash_table_destroy);
 
diff --git a/src/core/nm-netns.h b/src/core/nm-netns.h
index 43e9c781..5ddb852a 100644
--- a/src/core/nm-netns.h
+++ b/src/core/nm-netns.h
@@ -41,15 +41,22 @@ NML3Cfg *nm_netns_l3cfg_acquire(NMNetns *netns, int ifindex);
 
 /*****************************************************************************/
 
+typedef enum {
+    NM_NETNS_IP_RESERVATION_TYPE_SHARED4,
+
+    _NM_NETNS_IP_RESERVATION_TYPE_NUM,
+} NMNetnsIPReservationType;
+
 typedef struct {
-    in_addr_t addr;
-    int       _ref_count;
-    NMNetns  *_self;
-} NMNetnsSharedIPHandle;
+    in_addr_t                addr;
+    int                      _ref_count;
+    NMNetnsIPReservationType _type;
+    NMNetns                 *_self;
+} NMNetnsIPReservation;
 
-NMNetnsSharedIPHandle *nm_netns_shared_ip_reserve(NMNetns *self);
+NMNetnsIPReservation *nm_netns_ip_reservation_get(NMNetns *self, NMNetnsIPReservationType type);
 
-void nm_netns_shared_ip_release(NMNetnsSharedIPHandle *handle);
+void nm_netns_ip_reservation_release(NMNetnsIPReservation *reservation);
 
 /*****************************************************************************/
 
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index 0288795e..f7be1a9f 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -2353,7 +2353,10 @@ device_state_changed(NMDevice           *device,
         }
         if (sett_conn) {
             /* Reset auto retries back to default since connection was successful */
-            nm_manager_devcon_autoconnect_retries_reset(priv->manager, device, sett_conn);
+            nm_manager_devcon_autoconnect_reset_reconnect_all(priv->manager,
+                                                              device,
+                                                              sett_conn,
+                                                              FALSE);
         }
 
         /* Since there is no guarantee that device_l3cd_changed() is called
diff --git a/src/core/platform/tests/monitor.c b/src/core/platform/tests/monitor.c
index c83192bb..f413facf 100644
--- a/src/core/platform/tests/monitor.c
+++ b/src/core/platform/tests/monitor.c
@@ -186,6 +186,7 @@ ip_again:
                                00644,
                                NULL,
                                NULL,
+                               NULL,
                                NULL);
 
     nm_log_dbg(LOGD_PLATFORM, "dump to file complete");
diff --git a/src/core/platform/tests/test-link.c b/src/core/platform/tests/test-link.c
index fab6bd2e..77e7e641 100644
--- a/src/core/platform/tests/test-link.c
+++ b/src/core/platform/tests/test-link.c
@@ -123,7 +123,8 @@ software_add(NMLinkType link_type, const char *name)
         gboolean bond0_exists = !!nm_platform_link_get_by_ifname(NM_PLATFORM_GET, "bond0");
         int      r;
         const NMPlatformLnkBond nm_platform_lnk_bond_default = {
-            .mode = nmtst_rand_select(3, 1),
+            .mode        = nmtst_rand_select(3, 1),
+            .use_carrier = 1,
         };
 
         r = nm_platform_link_bond_add(NM_PLATFORM_GET, name, &nm_platform_lnk_bond_default, NULL);
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index f8f7070b..fbad2447 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -624,6 +624,79 @@ test_ip4_zero_gateway(void)
 }
 
 static void
+test_via(void)
+{
+    int                ifindex = nm_platform_link_get_ifindex(NM_PLATFORM_GET, DEVICE_NAME);
+    GPtrArray         *routes;
+    NMPlatformIP4Route rts[1];
+    struct in6_addr    gateway6;
+    const int          metric = 22987;
+    NMPlatformIP4Route route4;
+    guint              mss = 1000;
+    in_addr_t          net4;
+
+    /* Test IPv4 routes with a IPv6 gateway (using RTA_VIA attribute) */
+
+    inet_pton(AF_INET6, "fd01::1", &gateway6);
+    inet_pton(AF_INET, "1.2.3.4", &net4);
+
+    /* Add direct route to IPv6 gateway: ip route add dev $DEV fd01::1/128 */
+    nmtstp_ip6_route_add(NM_PLATFORM_GET,
+                         ifindex,
+                         NM_IP_CONFIG_SOURCE_USER,
+                         gateway6,
+                         128,
+                         in6addr_any,
+                         in6addr_any,
+                         metric,
+                         mss);
+    g_assert(nmtstp_ip6_route_get(NM_PLATFORM_GET, ifindex, &gateway6, 128, metric, NULL, 0));
+
+    /* Add IPv4 route via IPv6 gateway: ip route add dev $DEV 1.2.3.4/32 via inet6 fd01::1 */
+    route4 = (NMPlatformIP4Route) {
+        .ifindex         = ifindex,
+        .rt_source       = NM_IP_CONFIG_SOURCE_USER,
+        .network         = net4,
+        .plen            = 32,
+        .metric          = metric,
+        .via.addr_family = AF_INET6,
+        .via.addr.addr6  = gateway6,
+        .mss             = mss,
+    };
+    g_assert(NMTST_NM_ERR_SUCCESS(
+        nm_platform_ip4_route_add(NM_PLATFORM_GET, NMP_NLM_FLAG_REPLACE, &route4, NULL)));
+    g_assert(nmtstp_ip4_route_get(NM_PLATFORM_GET, ifindex, net4, 32, metric, 0));
+
+    /* Test route listing */
+    routes = nmtstp_ip4_route_get_all(NM_PLATFORM_GET, ifindex);
+    g_assert_cmpint(routes->len, ==, 1);
+
+    memset(rts, 0, sizeof(rts));
+    rts[0].rt_source       = nmp_utils_ip_config_source_round_trip_rtprot(NM_IP_CONFIG_SOURCE_USER);
+    rts[0].scope_inv       = nm_platform_route_scope_inv(RT_SCOPE_LINK);
+    rts[0].network         = net4;
+    rts[0].plen            = 32;
+    rts[0].ifindex         = ifindex;
+    rts[0].gateway         = INADDR_ANY;
+    rts[0].metric          = metric;
+    rts[0].mss             = mss;
+    rts[0].via.addr_family = AF_INET6;
+    rts[0].via.addr.addr6  = gateway6;
+    rts[0].n_nexthops      = 1;
+    nmtst_platform_ip4_routes_equal_aptr((const NMPObject *const *) routes->pdata,
+                                         rts,
+                                         routes->len,
+                                         TRUE);
+    g_ptr_array_unref(routes);
+
+    /* Delete routes */
+    g_assert(nmtstp_platform_ip6_route_delete(NM_PLATFORM_GET, ifindex, gateway6, 128, metric));
+    g_assert(!nmtstp_ip6_route_get(NM_PLATFORM_GET, ifindex, &gateway6, 128, metric, NULL, 0));
+    g_assert(nmtstp_platform_ip4_route_delete(NM_PLATFORM_GET, ifindex, net4, 32, metric));
+    g_assert(!nmtstp_ip4_route_get(NM_PLATFORM_GET, ifindex, net4, 32, metric, 0));
+}
+
+static void
 test_ip4_route_options(gconstpointer test_data)
 {
     const int TEST_IDX = GPOINTER_TO_INT(test_data);
@@ -2421,6 +2494,7 @@ _nmtstp_setup_tests(void)
         add_test_func("/route/ip4_route_get", test_ip4_route_get);
         add_test_func("/route/ip6_route_get", test_ip6_route_get);
         add_test_func("/route/ip4_zero_gateway", test_ip4_zero_gateway);
+        add_test_func("/route/via", test_via);
     }
 
     if (nmtstp_is_root_test()) {
diff --git a/src/core/settings/nm-settings-storage.h b/src/core/settings/nm-settings-storage.h
index 65750152..8847acc3 100644
--- a/src/core/settings/nm-settings-storage.h
+++ b/src/core/settings/nm-settings-storage.h
@@ -27,6 +27,19 @@
 
 struct _NMSettingsPlugin;
 
+/**
+ * NMSettingsStorage:
+ * @_plugin: The settings plugin that provides this storage.
+ * @_uuid: UUID of the profile represented by this storage.
+ * @_filename: Backing filename (can be NULL for in-memory or meta-data).
+ * @_storage_lst: Node in the per-plugin storage list.
+ * @_storage_by_uuid_lst: Node in the per-UUID storage list.
+ *
+ * Describes the origin and identity of one profile instance as provided by a
+ * specific settings plugin and (optionally) a backing file. A single UUID may
+ * have multiple storages from different plugins; plugin order determines
+ * priority.
+ */
 typedef struct NMSettingsStorage {
     GObject                   parent;
     struct _NMSettingsPlugin *_plugin;
diff --git a/src/core/settings/nm-settings.c b/src/core/settings/nm-settings.c
index 50d98ece..702c53d5 100644
--- a/src/core/settings/nm-settings.c
+++ b/src/core/settings/nm-settings.c
@@ -76,6 +76,17 @@ static NM_CACHED_QUARK_FCN("default-wired-connection-blocked",
 
 /*****************************************************************************/
 
+/**
+ * StorageData:
+ * @sd_lst: Node used in per-UUID storage lists.
+ * @storage: Storage provider instance for this UUID.
+ * @connection: Connection object backed by @storage, or NULL for meta-data.
+ * @prioritize: Request to prioritize this storage during merge.
+ *
+ * Per-UUID storage entry used to accumulate and merge updates from plugins.
+ * Items live temporarily in the dirty list and are merged into the current list
+ * with stable priority ordering.
+ */
 typedef struct _StorageData {
     CList              sd_lst;
     NMSettingsStorage *storage;
@@ -165,6 +176,20 @@ _storage_data_is_alive(StorageData *sd)
 
 /*****************************************************************************/
 
+/**
+ * SettConnEntry:
+ * @uuid: Normalized UUID key for this entry (points to @_uuid_data).
+ * @sett_conn: Current NMSettingsConnection selected for @uuid, or NULL.
+ * @storage: The storage that currently owns @sett_conn, or NULL.
+ * @sd_lst_head: Head of current storages list for @uuid (high to low priority).
+ * @dirty_sd_lst_head: Head of pending storage updates to merge.
+ * @sce_dirty_lst: Node in the global dirty queue.
+ * @_uuid_data: Inline storage backing @uuid.
+ *
+ * Tracks one connection profile across all storages and its dirty state.
+ * It holds the authoritative in-memory connection and the sets of storages
+ * providing or updating it.
+ */
 typedef struct {
     const char           *uuid;
     NMSettingsConnection *sett_conn;
@@ -1368,10 +1393,11 @@ _connection_changed_track(NMSettings        *self,
                           NMConnection      *connection,
                           gboolean           prioritize)
 {
-    NMSettingsPrivate *priv = NM_SETTINGS_GET_PRIVATE(self);
-    SettConnEntry     *sett_conn_entry;
-    StorageData       *sd;
-    const char        *uuid;
+    NMSettingsPrivate    *priv = NM_SETTINGS_GET_PRIVATE(self);
+    SettConnEntry        *sett_conn_entry;
+    StorageData          *sd;
+    const char           *uuid;
+    gs_free_error GError *error = NULL;
 
     nm_assert_valid_settings_storage(NULL, storage);
 
@@ -1382,6 +1408,17 @@ _connection_changed_track(NMSettings        *self,
               || (_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS));
     nm_assert(!connection || nm_streq0(uuid, nm_connection_get_uuid(connection)));
 
+    if (connection && !nm_utils_connection_supported(connection, &error)) {
+        _LOGD("storage[%s," NM_SETTINGS_STORAGE_PRINT_FMT
+              "]: ignoring connection \"%s\" from file \"%s\": %s",
+              uuid,
+              NM_SETTINGS_STORAGE_PRINT_ARG(storage),
+              nm_connection_get_id(connection),
+              nm_settings_storage_get_filename(storage),
+              error->message);
+        connection = NULL;
+    }
+
     nm_assert_connection_unchanging(connection);
 
     sett_conn_entry =
@@ -1851,6 +1888,9 @@ nm_settings_add_connection(NMSettings                     *self,
 
     NM_SET_OUT(out_sett_conn, NULL);
 
+    if (!nm_utils_connection_supported(connection, error))
+        return FALSE;
+
     uuid = nm_connection_get_uuid(connection);
 
     sett_conn_entry = _sett_conn_entries_get(self, uuid);
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 1e2e2d92..b9e3f919 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
@@ -2056,8 +2056,9 @@ make_ip4_setting(shvarFile *ifcfg,
          * Pick up just IPv4 addresses (IPv6 addresses are taken by make_ip6_setting())
          */
         for (i = 1; i < 10000; i++) {
-            NMDnsServer dns;
-            char        tag[256];
+            NMDnsServer           dns;
+            char                  tag[256];
+            gs_free_error GError *local = NULL;
 
             numbered_tag(tag, "DNS", i);
             nm_clear_g_free(&value);
@@ -2065,12 +2066,13 @@ make_ip4_setting(shvarFile *ifcfg,
             if (!v)
                 break;
 
-            if (!nm_dns_uri_parse(AF_UNSPEC, v, &dns)) {
+            if (!nm_dns_uri_parse(AF_UNSPEC, v, &dns, &local)) {
                 g_set_error(error,
                             NM_SETTINGS_ERROR,
                             NM_SETTINGS_ERROR_INVALID_CONNECTION,
-                            "Invalid DNS server address '%s'",
-                            v);
+                            "Invalid DNS server address '%s': %s",
+                            v,
+                            local->message);
                 return NULL;
             }
 
@@ -2607,8 +2609,9 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
      * Pick up just IPv6 addresses (IPv4 addresses are taken by make_ip4_setting())
      */
     for (i = 1; i < 10000; i++) {
-        NMDnsServer dns;
-        char        tag[256];
+        gs_free_error GError *err = NULL;
+        NMDnsServer           dns;
+        char                  tag[256];
 
         numbered_tag(tag, "DNS", i);
         nm_clear_g_free(&value);
@@ -2616,14 +2619,15 @@ make_ip6_setting(shvarFile *ifcfg, shvarFile *network_ifcfg, gboolean routes_rea
         if (!v)
             break;
 
-        if (!nm_dns_uri_parse(AF_UNSPEC, v, &dns)) {
+        if (!nm_dns_uri_parse(AF_UNSPEC, v, &dns, &err)) {
             if (is_disabled)
                 continue;
             g_set_error(error,
                         NM_SETTINGS_ERROR,
                         NM_SETTINGS_ERROR_INVALID_CONNECTION,
-                        "Invalid DNS server address '%s'",
-                        v);
+                        "Invalid DNS server address '%s': %s",
+                        v,
+                        err->message);
             return NULL;
         }
         if (dns.addr_family == AF_INET6) {
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 39cbddc5..21908090 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
@@ -320,6 +320,7 @@ write_blobs(GHashTable *blobs, GError **error)
                                         0600,
                                         NULL,
                                         NULL,
+                                        NULL,
                                         &write_error)) {
             g_set_error(error,
                         NM_SETTINGS_ERROR,
@@ -3626,6 +3627,14 @@ do_write_construct(NMConnection                   *connection,
 
     write_ip_routing_rules(connection, ifcfg, route_ignore);
 
+    if (nm_setting_connection_get_dnssec(s_con) != NM_SETTING_CONNECTION_DNSSEC_DEFAULT) {
+        set_error_unsupported(error,
+                              connection,
+                              NM_SETTING_CONNECTION_SETTING_NAME "." NM_SETTING_CONNECTION_DNSSEC,
+                              TRUE);
+        return FALSE;
+    }
+
     write_connection_setting(s_con, ifcfg, interface_name);
 
     NM_SET_OUT(out_ifcfg, g_steal_pointer(&ifcfg));
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-utils.c b/src/core/settings/plugins/keyfile/nms-keyfile-utils.c
index 7c0e329e..26fb3441 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-utils.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-utils.c
@@ -280,6 +280,7 @@ nms_keyfile_nmmeta_write(const char *dirname,
                                         length,
                                         0600,
                                         NULL,
+                                        NULL,
                                         &errsv,
                                         NULL)) {
             NM_SET_OUT(out_full_filename, g_steal_pointer(&full_filename_tmp));
diff --git a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
index b1dd2e44..c7c88260 100644
--- a/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
+++ b/src/core/settings/plugins/keyfile/nms-keyfile-writer.c
@@ -133,6 +133,7 @@ cert_writer(NMConnection                     *connection,
                                              0600,
                                              NULL,
                                              NULL,
+                                             NULL,
                                              &local);
         if (success) {
             /* Write the path value to the keyfile.
@@ -384,7 +385,14 @@ _internal_write_connection(NMConnection                   *connection,
         }
     }
 
-    nm_utils_file_set_contents(path, kf_content_buf, kf_content_len, 0600, NULL, NULL, &local_err);
+    nm_utils_file_set_contents(path,
+                               kf_content_buf,
+                               kf_content_len,
+                               0600,
+                               NULL,
+                               NULL,
+                               NULL,
+                               &local_err);
     if (local_err) {
         g_set_error(error,
                     NM_SETTINGS_ERROR,
diff --git a/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Write_GSM b/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Write_GSM
index 2f6dc26c..eec403cb 100644
--- a/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Write_GSM
+++ b/src/core/settings/plugins/keyfile/tests/keyfiles/Test_Write_GSM
@@ -8,6 +8,7 @@ timestamp=305415219
 [gsm]
 apn=internet2.voicestream.com
 device-id=da812de91eec16620b06cd0ca5cbc7ea25245222
+device-uid=MODEM1
 home-only=true
 network-id=254098
 password=parliament2
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 df5dfd40..badb40d3 100644
--- a/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
+++ b/src/core/settings/plugins/keyfile/tests/test-keyfile-settings.c
@@ -1408,6 +1408,8 @@ test_write_gsm_connection(void)
                  "89148000000060671234",
                  NM_SETTING_GSM_SIM_OPERATOR_ID,
                  "310260",
+                 NM_SETTING_GSM_DEVICE_UID,
+                 "MODEM1",
                  NULL);
 
     write_test_connection_and_reread(connection, TRUE, TEST_KEYFILES_DIR "/Test_Write_GSM");
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 56cc832b..233afe48 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -206,20 +206,30 @@ nm_supplicant_config_add_blob(NMSupplicantConfig *self,
     ConfigOption              *old_opt;
     ConfigOption              *opt;
     NMSupplOptType             type;
-    const guint8              *data;
     gsize                      data_len;
+    gs_free char              *full_value = NULL;
 
     g_return_val_if_fail(NM_IS_SUPPLICANT_CONFIG(self), FALSE);
     g_return_val_if_fail(key != NULL, FALSE);
     g_return_val_if_fail(value != NULL, FALSE);
     g_return_val_if_fail(blobid != NULL, FALSE);
 
-    data = g_bytes_get_data(value, &data_len);
+    g_bytes_get_data(value, &data_len);
     g_return_val_if_fail(data_len > 0, FALSE);
 
-    priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
+    if (data_len > 32 * 1024 * 1024) {
+        g_set_error(error,
+                    NM_SUPPLICANT_ERROR,
+                    NM_SUPPLICANT_ERROR_CONFIG,
+                    "blob '%s' is larger than 32MiB",
+                    key);
+        return FALSE;
+    }
+
+    priv       = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
+    full_value = g_strdup_printf("blob://%s", blobid);
 
-    type = nm_supplicant_settings_verify_setting(key, (const char *) data, data_len);
+    type = nm_supplicant_settings_verify_setting(key, full_value, strlen(full_value));
     if (type == NM_SUPPL_OPT_TYPE_INVALID) {
         g_set_error(error,
                     NM_SUPPLICANT_ERROR,
@@ -240,7 +250,7 @@ nm_supplicant_config_add_blob(NMSupplicantConfig *self,
     }
 
     opt        = g_slice_new0(ConfigOption);
-    opt->value = g_strdup_printf("blob://%s", blobid);
+    opt->value = g_steal_pointer(&full_value);
     opt->len   = strlen(opt->value);
     opt->type  = type;
 
@@ -521,6 +531,7 @@ get_ap_params(guint                         freq,
     case NM_SETTING_WIRELESS_CHANNEL_WIDTH_80MHZ:
     {
         guint channel;
+        guint center_channel = 0;
 
         if (freq < 5000) {
             /* the setting is not valid */
@@ -530,12 +541,29 @@ get_ap_params(guint                         freq,
 
         /* Determine the center channel according to the table at
          * https://en.wikipedia.org/wiki/List_of_WLAN_channels */
+
         channel = (freq - 5000) / 5;
-        channel = ((channel / 4 - 1) / 4) * 16 + 10;
 
-        *out_ht40             = 1;
-        *out_max_oper_chwidth = 1;
-        *out_center_freq      = 5000 + 5 * channel;
+        if (channel >= 36 && channel <= 48)
+            center_channel = 42;
+        else if (channel >= 52 && channel <= 64)
+            center_channel = 58;
+        else if (channel >= 100 && channel <= 112)
+            center_channel = 106;
+        else if (channel >= 116 && channel <= 128)
+            center_channel = 122;
+        else if (channel >= 132 && channel <= 144)
+            center_channel = 138;
+        else if (channel >= 149 && channel <= 161)
+            center_channel = 155;
+        else if (channel >= 165 && channel <= 177)
+            center_channel = 171;
+
+        if (center_channel) {
+            *out_ht40             = 1;
+            *out_max_oper_chwidth = 1;
+            *out_center_freq      = 5000 + 5 * center_channel;
+        }
 
         return;
     }
@@ -1016,7 +1044,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
         if (_get_capability(priv, NM_SUPPL_CAP_TYPE_SAE)
             && _get_capability(priv, NM_SUPPL_CAP_TYPE_PMF)
             && _get_capability(priv, NM_SUPPL_CAP_TYPE_BIP)
-            && (!is_ap || pmf != NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE)) {
+            && (pmf != NM_SETTING_WIRELESS_SECURITY_PMF_DISABLE)) {
             g_string_append(key_mgmt_conf, " SAE");
             if (!is_ap && _get_capability(priv, NM_SUPPL_CAP_TYPE_FT))
                 g_string_append(key_mgmt_conf, " FT-SAE");
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index ef72447a..5c60a7b6 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -66,6 +66,7 @@ enum {
     WPS_CREDENTIALS, /* WPS credentials received */
     GROUP_STARTED,   /* a new Group (interface) was created */
     GROUP_FINISHED,  /* a Group (interface) has been finished */
+    PSK_MISMATCH,    /* supplicant reported incorrect PSK */
     LAST_SIGNAL
 };
 
@@ -3232,6 +3233,10 @@ _signal_handle(NMSupplicantInterface *self,
             return;
         }
 
+        if (nm_streq(signal_name, "PskMismatch")) {
+            g_signal_emit(self, signals[PSK_MISMATCH], 0);
+            return;
+        }
         return;
     }
 
@@ -3864,4 +3869,14 @@ nm_supplicant_interface_class_init(NMSupplicantInterfaceClass *klass)
                                            G_TYPE_NONE,
                                            1,
                                            G_TYPE_STRING);
+
+    signals[PSK_MISMATCH] = g_signal_new(NM_SUPPLICANT_INTERFACE_PSK_MISMATCH,
+                                         G_OBJECT_CLASS_TYPE(object_class),
+                                         G_SIGNAL_RUN_LAST,
+                                         0,
+                                         NULL,
+                                         NULL,
+                                         NULL,
+                                         G_TYPE_NONE,
+                                         0);
 }
diff --git a/src/core/supplicant/nm-supplicant-interface.h b/src/core/supplicant/nm-supplicant-interface.h
index b8d9b013..961de1b1 100644
--- a/src/core/supplicant/nm-supplicant-interface.h
+++ b/src/core/supplicant/nm-supplicant-interface.h
@@ -86,6 +86,7 @@ typedef enum {
 #define NM_SUPPLICANT_INTERFACE_WPS_CREDENTIALS "wps-credentials"
 #define NM_SUPPLICANT_INTERFACE_GROUP_STARTED   "group-started"
 #define NM_SUPPLICANT_INTERFACE_GROUP_FINISHED  "group-finished"
+#define NM_SUPPLICANT_INTERFACE_PSK_MISMATCH    "wpa-psk-mismatch"
 
 typedef struct _NMSupplicantInterfaceClass NMSupplicantInterfaceClass;
 
diff --git a/src/core/tests/meson.build b/src/core/tests/meson.build
index 78c689f7..31acad94 100644
--- a/src/core/tests/meson.build
+++ b/src/core/tests/meson.build
@@ -6,6 +6,7 @@ test_units = [
   'test-core',
   'test-core-with-expect',
   'test-dcb',
+  'test-netns',
   'test-l3cfg',
   'test-utils',
   'test-wired-defname',
diff --git a/src/core/tests/test-netns.c b/src/core/tests/test-netns.c
new file mode 100644
index 00000000..26ecbcb8
--- /dev/null
+++ b/src/core/tests/test-netns.c
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "src/core/nm-default-daemon.h"
+
+#include "nm-netns.h"
+#include "nm-test-utils-core.h"
+
+static void
+test_ip_reservation_shared4(void)
+{
+    gs_unref_object NMPlatform *platform = NULL;
+    gs_unref_object NMNetns    *netns    = NULL;
+    NMNetnsIPReservation       *res[256];
+    NMNetnsIPReservation       *res1;
+    NMNetnsIPReservation       *res2;
+    char                        buf[NM_INET_ADDRSTRLEN];
+    guint                       i;
+
+    platform = g_object_ref(NM_PLATFORM_GET);
+    netns    = nm_netns_new(platform);
+
+    /* Allocate addresses from 10.42.0.1 to 10.42.255.1 */
+    for (i = 0; i < 256; i++) {
+        res[i] = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_SHARED4);
+        g_snprintf(buf, sizeof(buf), "10.42.%u.1", i);
+        nmtst_assert_ip4_address(res[i]->addr, buf);
+        g_assert_cmpint(res[i]->_ref_count, ==, 1);
+    }
+
+    /* Release an address and get it back */
+    nm_netns_ip_reservation_release(res[139]);
+    res[139] = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_SHARED4);
+    nmtst_assert_ip4_address(res[139]->addr, "10.42.139.1");
+
+    /* Reuse 10.42.255.1 once */
+    NMTST_EXPECT_NM_ERROR("netns[*]: shared-ip4: ran out of IP addresses. Reuse 10.42.255.1/24");
+    res1 = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_SHARED4);
+    g_test_assert_expected_messages();
+    nmtst_assert_ip4_address(res1->addr, "10.42.255.1");
+    g_assert_cmpint(res1->_ref_count, ==, 2);
+
+    /* Reuse 10.42.255.1 twice */
+    res2 = nm_netns_ip_reservation_get(netns, NM_NETNS_IP_RESERVATION_TYPE_SHARED4);
+    g_assert(res2 == res1);
+    nmtst_assert_ip4_address(res1->addr, "10.42.255.1");
+    g_assert_cmpint(res2->_ref_count, ==, 3);
+
+    /* Release all */
+    nm_netns_ip_reservation_release(res1);
+    nm_netns_ip_reservation_release(res2);
+    for (i = 0; i < 256; i++) {
+        nm_netns_ip_reservation_release(res[i]);
+    }
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    nmtst_init_with_logging(&argc, &argv, NULL, "ALL");
+    nm_linux_platform_setup();
+
+    g_test_add_func("/netns/ip_reservation/shared4", test_ip_reservation_shared4);
+
+    return g_test_run();
+}
diff --git a/src/core/tests/test-utils.c b/src/core/tests/test-utils.c
index 2bcb6f69..0418d3ae 100644
--- a/src/core/tests/test-utils.c
+++ b/src/core/tests/test-utils.c
@@ -261,6 +261,70 @@ test_shorten_hostname(void)
 }
 
 /*****************************************************************************/
+typedef struct {
+    NMRateLimit ratelimit;
+    GMainLoop  *loop;
+    GSource    *source;
+    guint       num;
+} RateLimitData;
+
+static int
+rate_limit_window_expire_cb(gpointer user_data)
+{
+    RateLimitData *data = user_data;
+
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+
+    g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5));
+
+    nm_clear_g_source_inst(&data->source);
+    g_main_loop_quit(data->loop);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static int
+rate_limit_check_cb(gpointer user_data)
+{
+    RateLimitData *data = user_data;
+
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(nm_rate_limit_check(&data->ratelimit, 1, 5));
+
+    g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5));
+    g_assert(!nm_rate_limit_check(&data->ratelimit, 1, 5));
+
+    nm_clear_g_source_inst(&data->source);
+    data->source = nm_g_timeout_add_source(1000, rate_limit_window_expire_cb, data);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+test_rate_limit_check(void)
+{
+    RateLimitData data;
+
+    data = (RateLimitData) {
+        .loop      = g_main_loop_new(NULL, FALSE),
+        .ratelimit = {},
+        .source    = nm_g_timeout_add_source(1, rate_limit_check_cb, &data),
+        .num       = 0,
+    };
+
+    g_main_loop_run(data.loop);
+    g_main_loop_unref(data.loop);
+}
+
+/*****************************************************************************/
 
 NMTST_DEFINE();
 
@@ -272,6 +336,7 @@ main(int argc, char **argv)
     g_test_add_func("/utils/stable_privacy", test_stable_privacy);
     g_test_add_func("/utils/hw_addr_gen_stable_eth", test_hw_addr_gen_stable_eth);
     g_test_add_func("/utils/shorten-hostname", test_shorten_hostname);
+    g_test_add_func("/utils/rate-limit-check", test_rate_limit_check);
 
     return g_test_run();
 }
diff --git a/src/core/vpn/nm-vpn-connection.c b/src/core/vpn/nm-vpn-connection.c
index 22364ef9..54478c53 100644
--- a/src/core/vpn/nm-vpn-connection.c
+++ b/src/core/vpn/nm-vpn-connection.c
@@ -26,10 +26,12 @@
 #include "nm-active-connection.h"
 #include "nm-config.h"
 #include "nm-dbus-manager.h"
+#include "devices/nm-device.h"
 #include "nm-dispatcher.h"
 #include "nm-firewalld-manager.h"
 #include "nm-ip-config.h"
 #include "nm-l3-config-data.h"
+#include "nm-manager.h"
 #include "nm-netns.h"
 #include "nm-pacrunner-manager.h"
 #include "nm-vpn-manager.h"
@@ -173,6 +175,7 @@ typedef struct {
     };
 
     GSource           *init_fail_on_idle_source;
+    GSource           *check_device_added_idle_source;
     GSource           *connect_timeout_source;
     GCancellable      *main_cancellable;
     GVariant          *connect_hash;
@@ -227,6 +230,8 @@ static void _set_vpn_state(NMVpnConnection              *self,
 static void
 _l3cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMVpnConnection *self);
 
+static void _check_complete(NMVpnConnection *self, gboolean success);
+
 /*****************************************************************************/
 
 #define _NMLOG_DOMAIN      LOGD_VPN
@@ -1403,15 +1408,29 @@ fw_change_zone_cb(NMFirewalldManager       *firewalld_manager,
     _apply_config(self);
 }
 
+static gboolean
+_check_device_added_idle_cb(gpointer user_data)
+{
+    NMVpnConnection        *self = user_data;
+    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
+
+    _check_complete(self, TRUE);
+    nm_clear_g_source_inst(&priv->check_device_added_idle_source);
+
+    return G_SOURCE_CONTINUE;
+}
+
 static void
 _check_complete(NMVpnConnection *self, gboolean success)
 {
     NMVpnConnectionPrivate                 *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     NMConnection                           *connection;
+    NMDevice                               *device;
     NMSettingConnection                    *s_con;
     const char                             *zone;
     const char                             *iface;
+    int                                     ifindex;
 
     if (priv->vpn_state < STATE_IP_CONFIG_GET || priv->vpn_state > STATE_ACTIVATED)
         return;
@@ -1437,10 +1456,34 @@ _check_complete(NMVpnConnection *self, gboolean success)
     }
 
     connection = _get_applied_connection(self);
+    ifindex    = nm_vpn_connection_get_ip_ifindex(self, FALSE);
+    device     = nm_manager_get_device_by_ifindex(NM_MANAGER_GET, ifindex);
+
+    /* We have a defined interface index, but the device is not processed yet.
+     * The processing of the new kernel link could be queued in an idle handler,
+     * so schedule an idle handler once to check if the device has been processed.
+     */
+    if (ifindex > 0 && !device && !priv->check_device_added_idle_source) {
+        priv->check_device_added_idle_source =
+            nm_g_idle_add_source(_check_device_added_idle_cb, self);
+        return;
+    }
 
-    l3cd = nm_l3_config_data_new_from_connection(nm_netns_get_multi_idx(priv->netns),
-                                                 nm_vpn_connection_get_ip_ifindex(self, TRUE),
-                                                 connection);
+    /* Use nm_device_create_l3_config_data_from_connection here if possible. This ensures that
+     * connection properties like mdns, llmnr, dns-over-tls or dnssec are applied to vpn connections
+     * If this vpn connection does not have its own device resort to nm_l3_config_data_new_from_connection
+     * since we can't properly apply these properties anyway
+     */
+    if (ifindex > 0) {
+        nm_assert(device);
+        l3cd = nm_device_create_l3_config_data_from_connection(device, connection);
+    } else {
+        l3cd = nm_l3_config_data_new_from_connection(nm_netns_get_multi_idx(priv->netns),
+                                                     nm_vpn_connection_get_ip_ifindex(self, TRUE),
+                                                     connection);
+        _LOGD("VPN connection does not have its own device. Some connection properties won't be "
+              "supported.");
+    }
 
     nm_l3_config_data_set_allow_routes_without_address(l3cd, AF_INET, TRUE);
     nm_l3_config_data_set_allow_routes_without_address(l3cd, AF_INET6, TRUE);
@@ -3025,6 +3068,8 @@ dispose(GObject *object)
 
     nm_clear_g_source_inst(&priv->init_fail_on_idle_source);
 
+    nm_clear_g_source_inst(&priv->check_device_added_idle_source);
+
     nm_clear_g_cancellable(&priv->main_cancellable);
 
     nm_clear_g_source_inst(&priv->start_timeout_source);