about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-08-13 16:40:06 +0200
committerMichael Biebl <biebl@debian.org>2022-08-13 16:40:06 +0200
commit38ca7ae9c3556e9a5b50ee15015ea194e6ca8da1 (patch)
tree99f19f118f550362c3288e7b82a7aab3a75c8d64 /src
parentc2473cb3f165975f2899ccef11f036c9762a8aa3 (diff)
parent49ac65f875a36d7ebcbc8270cf0fc60acbb2e052 (diff)
Merge tag 'debian/1.38.4-1' into debian/bullseye-backports
network-manager Debian release 1.38.4-1
Diffstat (limited to 'src')
-rw-r--r--src/c-rbtree/src/c-rbtree.c21
-rw-r--r--src/core/devices/nm-device-bridge.c3
-rw-r--r--src/core/devices/nm-device-ppp.c37
-rw-r--r--src/core/devices/nm-device.c78
-rw-r--r--src/core/devices/nm-device.h2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-bridge.c2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-interface.c2
-rw-r--r--src/core/devices/ovs/nm-device-ovs-port.c2
-rw-r--r--src/core/devices/ovs/nm-ovs-factory.c6
-rw-r--r--src/core/devices/wifi/nm-device-wifi.c46
-rw-r--r--src/core/devices/wifi/nm-iwd-manager.c2
-rw-r--r--src/core/devices/wwan/nm-device-modem.c8
-rw-r--r--src/core/devices/wwan/nm-modem-broadband.c5
-rw-r--r--src/core/dhcp/nm-dhcp-client.c740
-rw-r--r--src/core/dhcp/nm-dhcp-client.h73
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c19
-rw-r--r--src/core/dhcp/nm-dhcp-listener.c22
-rw-r--r--src/core/dhcp/nm-dhcp-manager.c11
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c173
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c25
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c3
-rw-r--r--src/core/nm-core-utils.c9
-rw-r--r--src/core/nm-l3-config-data.c5
-rw-r--r--src/core/nm-l3cfg.c95
-rw-r--r--src/core/nm-l3cfg.h5
-rw-r--r--src/core/nm-policy.c17
-rw-r--r--src/core/platform/tests/test-platform-general.c288
-rw-r--r--src/core/platform/tests/test-route.c5
-rw-r--r--src/core/ppp/nm-ppp-manager.c6
-rw-r--r--src/core/supplicant/nm-supplicant-config.c8
-rw-r--r--src/core/supplicant/nm-supplicant-interface.c43
-rw-r--r--src/core/supplicant/nm-supplicant-settings-verify.c1
-rw-r--r--src/core/tests/test-core.c23
-rw-r--r--src/libnm-client-impl/nm-client.c8
-rw-r--r--src/libnm-client-impl/nm-libnm-utils.c4
-rw-r--r--src/libnm-core-impl/nm-setting-wireless.c4
-rw-r--r--src/libnm-core-impl/tests/certs/test-aes-128-ec-key.pem8
-rw-r--r--src/libnm-core-impl/tests/certs/test-aes-256-ec-key.pem8
-rw-r--r--src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.derbin0 -> 121 bytes
-rw-r--r--src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.pem5
-rw-r--r--src/libnm-core-impl/tests/test-crypto.c15
-rw-r--r--src/libnm-core-public/nm-version-macros.h2
-rw-r--r--src/libnm-crypto/nm-crypto.c7
-rw-r--r--src/libnm-crypto/nm-crypto.h3
-rw-r--r--src/libnm-glib-aux/nm-hash-utils.c2
-rw-r--r--src/libnm-glib-aux/nm-random-utils.c340
-rw-r--r--src/libnm-glib-aux/nm-random-utils.h10
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.c19
-rw-r--r--src/libnm-glib-aux/nm-shared-utils.h31
-rw-r--r--src/libnm-glib-aux/nm-uuid.c4
-rw-r--r--src/libnm-glib-aux/tests/test-shared-general.c10
-rw-r--r--src/libnm-platform/nm-platform.h119
-rw-r--r--src/libnm-systemd-core/meson.build1
-rw-r--r--src/libnm-systemd-core/nm-sd-utils-core.c27
-rw-r--r--src/libnm-systemd-core/nm-sd-utils-core.h17
-rw-r--r--src/nmcli/devices.c5
-rw-r--r--src/nmcli/nmcli-completion2
57 files changed, 1529 insertions, 907 deletions
diff --git a/src/c-rbtree/src/c-rbtree.c b/src/c-rbtree/src/c-rbtree.c
index 2f0e608f..28de4d95 100644
--- a/src/c-rbtree/src/c-rbtree.c
+++ b/src/c-rbtree/src/c-rbtree.c
@@ -35,12 +35,25 @@
  * CRBNode is 4-byte aligned, so the lower 2 bits are actually unused. We also
  * sometimes store a pointer to the root-node, so make sure this one is also 4
  * byte aligned.
- * Note that there are actually some architectures where `max_align_t` is 4, so
- * we do not have much wiggle-room to extend this flag-set.
+ *
+ * Additionally, we want to avoid an alignment that is bigger than the
+ * alignment guaranteed by the system allocator or supported by the system
+ * linker. As there is no standard way to check this, we simply verify against
+ * `alignof(max_align_t)`, as this alignment must be supported by the
+ * toolchain.
+ *
+ * m64k is special here, as it only has a 2-byte max-alignment, but still
+ * guarantees a >=4-byte alignment on allocations. So hard-code the maximum for
+ * it.
  */
-static_assert(alignof(CRBNode) <= alignof(max_align_t), "Invalid RBNode alignment");
+#ifdef __m68k__
+#  define C_RBTREE_MAX_ALIGN (C_MAX(4, alignof(max_align_t)))
+#else
+#  define C_RBTREE_MAX_ALIGN (alignof(max_align_t))
+#endif
+static_assert(alignof(CRBNode) <= C_RBTREE_MAX_ALIGN, "Invalid RBNode alignment");
 static_assert(alignof(CRBNode) >= 4, "Invalid CRBNode alignment");
-static_assert(alignof(CRBTree) <= alignof(max_align_t), "Invalid RBTree alignment");
+static_assert(alignof(CRBTree) <= C_RBTREE_MAX_ALIGN, "Invalid RBTree alignment");
 static_assert(alignof(CRBTree) >= 4, "Invalid CRBTree alignment");
 
 /**
diff --git a/src/core/devices/nm-device-bridge.c b/src/core/devices/nm-device-bridge.c
index f11c172a..9bf7ac01 100644
--- a/src/core/devices/nm-device-bridge.c
+++ b/src/core/devices/nm-device-bridge.c
@@ -994,7 +994,8 @@ enslave_slave(NMDevice *device, NMDevice *slave, NMConnection *connection, gbool
         nm_assert(s_bridge);
         s_port = nm_connection_get_setting_bridge_port(connection);
 
-        bridge_set_vlan_options(device, s_bridge);
+        if (!nm_device_sys_iface_state_is_external(device))
+            bridge_set_vlan_options(device, s_bridge);
 
         if (nm_setting_bridge_get_vlan_filtering(s_bridge)) {
             gs_free const NMPlatformBridgeVlan **plat_vlans = NULL;
diff --git a/src/core/devices/nm-device-ppp.c b/src/core/devices/nm-device-ppp.c
index 61e32348..6615b65e 100644
--- a/src/core/devices/nm-device-ppp.c
+++ b/src/core/devices/nm-device-ppp.c
@@ -23,6 +23,13 @@
 
 typedef struct _NMDevicePppPrivate {
     NMPppMgr *ppp_mgr;
+    union {
+        struct {
+            NML3CfgBlockHandle *l3cfg_block_handle_6;
+            NML3CfgBlockHandle *l3cfg_block_handle_4;
+        };
+        NML3CfgBlockHandle *l3cfg_block_handle_x[2];
+    };
 } NMDevicePppPrivate;
 
 struct _NMDevicePpp {
@@ -69,8 +76,10 @@ _ppp_mgr_stage3_maybe_ready(NMDevicePpp *self)
         const NMPppMgrIPData *ip_data;
 
         ip_data = nm_ppp_mgr_get_ip_data(priv->ppp_mgr, addr_family);
-        if (ip_data->ip_received)
+        if (ip_data->ip_received) {
+            nm_clear_pointer(&priv->l3cfg_block_handle_x[IS_IPv4], nm_l3cfg_unblock_obj_pruning);
             nm_device_devip_set_state(device, addr_family, NM_DEVICE_IP_STATE_READY, ip_data->l3cd);
+        }
     }
 
     if (nm_ppp_mgr_get_state(priv->ppp_mgr) >= NM_PPP_MGR_STATE_HAVE_IP_CONFIG)
@@ -80,9 +89,10 @@ _ppp_mgr_stage3_maybe_ready(NMDevicePpp *self)
 static void
 _ppp_mgr_callback(NMPppMgr *ppp_mgr, const NMPppMgrCallbackData *callback_data, gpointer user_data)
 {
-    NMDevicePpp  *self   = NM_DEVICE_PPP(user_data);
-    NMDevice     *device = NM_DEVICE(self);
-    NMDeviceState device_state;
+    NMDevicePpp        *self   = NM_DEVICE_PPP(user_data);
+    NMDevice           *device = NM_DEVICE(self);
+    NMDevicePppPrivate *priv   = NM_DEVICE_PPP_GET_PRIVATE(self);
+    NMDeviceState       device_state;
 
     if (callback_data->callback_type != NM_PPP_MGR_CALLBACK_TYPE_STATE_CHANGED)
         return;
@@ -112,6 +122,19 @@ _ppp_mgr_callback(NMPppMgr *ppp_mgr, const NMPppMgrCallbackData *callback_data,
                 return;
             }
 
+            /* pppd also tries to configure addresses by itself through some
+             * ioctls. If we remove between those calls an address that was added,
+             * pppd fails and quits. Temporarily block the removal of addresses
+             * and routes. */
+            if (!priv->l3cfg_block_handle_4) {
+                priv->l3cfg_block_handle_4 =
+                    nm_l3cfg_block_obj_pruning(nm_device_get_l3cfg(device), AF_INET);
+            }
+            if (!priv->l3cfg_block_handle_6) {
+                priv->l3cfg_block_handle_6 =
+                    nm_l3cfg_block_obj_pruning(nm_device_get_l3cfg(device), AF_INET6);
+            }
+
             if (old_name)
                 nm_manager_remove_device(NM_MANAGER_GET, old_name, NM_DEVICE_TYPE_PPP);
 
@@ -257,7 +280,11 @@ create_and_realize(NMDevice              *device,
 static void
 deactivate(NMDevice *device)
 {
-    NMDevicePpp *self = NM_DEVICE_PPP(device);
+    NMDevicePpp        *self = NM_DEVICE_PPP(device);
+    NMDevicePppPrivate *priv = NM_DEVICE_PPP_GET_PRIVATE(self);
+
+    nm_clear_pointer(&priv->l3cfg_block_handle_4, nm_l3cfg_unblock_obj_pruning);
+    nm_clear_pointer(&priv->l3cfg_block_handle_6, nm_l3cfg_unblock_obj_pruning);
 
     _ppp_mgr_cleanup(self);
 }
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index 70b52b1a..6b854517 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -1286,7 +1286,7 @@ out_fail:
               duid,
               duid_error);
 
-        nm_utils_random_bytes(&uuid, sizeof(uuid));
+        nm_random_get_bytes(&uuid, sizeof(uuid));
         duid_out = nm_utils_generate_duid_uuid(&uuid);
     }
 
@@ -2022,7 +2022,7 @@ out_fail:
           fail_reason);
     client_id_buf    = g_malloc(1 + 15);
     client_id_buf[0] = 0;
-    nm_utils_random_bytes(&client_id_buf[1], 15);
+    nm_random_get_bytes(&client_id_buf[1], 15);
     result = g_bytes_new_take(client_id_buf, 1 + 15);
 
 out_good:
@@ -4026,7 +4026,6 @@ _set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
     NMDevicePrivate         *priv                  = NM_DEVICE_GET_PRIVATE(self);
     gs_unref_object NML3Cfg *l3cfg_old             = NULL;
     NML3CfgCommitTypeHandle *l3cfg_commit_type_old = NULL;
-    gboolean                 l3_changed;
     int                      ip_ifindex_new;
     int                     *p_ifindex;
     gboolean                 l3cfg_was_reset = FALSE;
@@ -4067,6 +4066,10 @@ _set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
             l3cfg_was_reset       = TRUE;
         }
     }
+
+    if (!priv->l3cfg && l3cfg_old)
+        _dev_l3_register_l3cds(self, l3cfg_old, FALSE, FALSE);
+
     if (!priv->l3cfg && ip_ifindex_new > 0) {
         priv->l3cfg_ = nm_netns_l3cfg_acquire(priv->netns, ip_ifindex_new);
 
@@ -4078,6 +4081,7 @@ _set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
         _dev_l3_cfg_commit_type_reset(self);
         l3cfg_was_reset = TRUE;
     }
+
     if (!priv->l3cfg) {
         _cleanup_ip_pre(self, AF_INET, CLEANUP_TYPE_KEEP, FALSE);
         _cleanup_ip_pre(self, AF_INET6, CLEANUP_TYPE_KEEP, FALSE);
@@ -4118,11 +4122,7 @@ _set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
         _notify(self, PROP_IP6_CONFIG);
     }
 
-    if (l3cfg_old != priv->l3cfg) {
-        l3_changed = FALSE;
-        if (_dev_l3_register_l3cds(self, l3cfg_old, FALSE, FALSE))
-            l3_changed = TRUE;
-
+    if (priv->l3cfg && l3cfg_old != priv->l3cfg) {
         /* Now it gets ugly. We changed the ip-ifindex, which determines the NML3Cfg instance.
          * But all the NML3ConfigData we currently track are still for the old ifindex. We
          * need to update them.
@@ -4131,12 +4131,10 @@ _set_ifindex(NMDevice *self, int ifindex, gboolean is_ip_ifindex)
          * associated with one ifindex (and not the ifindex/ip-ifindex split). Or it
          * is not at all associated with an ifindex, but only a controlling device for
          * a real NMDevice (that has the ifindex). */
+
         _dev_l3_update_l3cds_ifindex(self);
 
         if (_dev_l3_register_l3cds(self, priv->l3cfg, TRUE, FALSE))
-            l3_changed = TRUE;
-
-        if (l3_changed)
             _dev_l3_cfg_commit(self, TRUE);
     }
 
@@ -6164,10 +6162,6 @@ carrier_changed(NMDevice *self, gboolean carrier)
 
     if (nm_device_is_master(self)) {
         if (carrier) {
-            /* Force master to retry getting ip addresses when carrier
-             * is restored. */
-            if (priv->state == NM_DEVICE_STATE_ACTIVATED)
-                nm_device_update_dynamic_ip_setup(self);
             /* If needed, also resume IP configuration that is
              * waiting for carrier. */
             if (priv->state == NM_DEVICE_STATE_IP_CONFIG)
@@ -6195,12 +6189,6 @@ carrier_changed(NMDevice *self, gboolean carrier)
              * the device.
              */
             nm_device_emit_recheck_auto_activate(self);
-        } else if (priv->state == NM_DEVICE_STATE_ACTIVATED) {
-            /* If the device is active without a carrier (probably because it is
-             * tagged for carrier ignore) ensure that when the carrier appears we
-             * renew DHCP leases and such.
-             */
-            nm_device_update_dynamic_ip_setup(self);
         }
     } else {
         if (priv->state == NM_DEVICE_STATE_UNAVAILABLE) {
@@ -6588,6 +6576,14 @@ device_link_changed(gpointer user_data)
         if (priv->state >= NM_DEVICE_STATE_IP_CONFIG && priv->state <= NM_DEVICE_STATE_ACTIVATED
             && !nm_device_sys_iface_state_is_external(self))
             nm_device_l3cfg_commit(self, NM_L3_CFG_COMMIT_TYPE_REAPPLY, FALSE);
+
+        /* If the device is active without a carrier (probably because it is
+         * tagged for carrier ignore) ensure that when the carrier appears we
+         * renew DHCP leases and such.
+         */
+        if (priv->state == NM_DEVICE_STATE_ACTIVATED) {
+            nm_device_update_dynamic_ip_setup(self);
+        }
     }
 
     if (update_unmanaged_specs)
@@ -7861,7 +7857,8 @@ nm_device_slave_notify_release(NMDevice *self, NMDeviceStateReason reason)
 void
 nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config)
 {
-    NMDevicePrivate *priv;
+    NMDevicePrivate      *priv;
+    const NML3ConfigData *l3cd_old;
 
     g_return_if_fail(NM_IS_DEVICE(self));
 
@@ -7879,6 +7876,18 @@ nm_device_removed(NMDevice *self, gboolean unconfigure_ip_config)
     }
 
     _dev_l3_register_l3cds(self, priv->l3cfg, FALSE, unconfigure_ip_config);
+
+    /* _dev_l3_register_l3cds() schedules a commit, but if the device has
+     * commit type NONE, that doesn't emit a l3cd-changed. Do it manually,
+     * to ensure that entries are removed from the DNS manager. */
+    if (priv->l3cfg
+        && NM_IN_SET(priv->sys_iface_state,
+                     NM_DEVICE_SYS_IFACE_STATE_REMOVED,
+                     NM_DEVICE_SYS_IFACE_STATE_EXTERNAL)) {
+        l3cd_old = nm_l3cfg_get_combined_l3cd(priv->l3cfg, TRUE);
+        if (l3cd_old)
+            g_signal_emit(self, signals[L3CD_CHANGED], 0, l3cd_old, NULL);
+    }
 }
 
 static gboolean
@@ -10117,13 +10126,6 @@ _dev_ipdhcpx_start(NMDevice *self, int addr_family)
 
     hwaddr = nmp_link_address_get_as_bytes(&pllink->l_address);
 
-    if (!IS_IPv4) {
-        if (!hwaddr) {
-            fail_reason = "interface has no MAC address to start DHCPv6";
-            goto out_fail;
-        }
-    }
-
     request_broadcast = FALSE;
     if (pllink) {
         str = nmp_object_link_udev_device_get_property_value(NMP_OBJECT_UP_CAST(pllink),
@@ -11393,8 +11395,8 @@ _dev_ipac6_start(NMDevice *self)
 
     if (node_type == NM_NDISC_NODE_TYPE_ROUTER)
         _dev_ipac6_set_state(self, NM_DEVICE_IP_STATE_READY);
-    else
-        _dev_ipac6_grace_period_start(self, ra_timeout, TRUE);
+
+    _dev_ipac6_grace_period_start(self, ra_timeout, TRUE);
 
     nm_ndisc_start(priv->ipac6_data.ndisc);
 }
@@ -11634,7 +11636,7 @@ activate_stage3_ip_config_for_addr_family(NMDevice *self, int addr_family, const
         priv->ip_data_x[IS_IPv4].wait_for_ports = FALSE;
     }
 
-    if (klass->ready_for_ip_config && !klass->ready_for_ip_config(self))
+    if (klass->ready_for_ip_config && !klass->ready_for_ip_config(self, FALSE))
         goto out_devip;
 
     if (IS_IPv4) {
@@ -11898,7 +11900,7 @@ activate_stage3_ip_config(NMDevice *self)
     }
 
     if (!nm_device_sys_iface_state_is_external(self)
-        && (!klass->ready_for_ip_config || klass->ready_for_ip_config(self))) {
+        && (!klass->ready_for_ip_config || klass->ready_for_ip_config(self, TRUE))) {
         if (priv->ipmanual_data.state_6 == NM_DEVICE_IP_STATE_NONE
             && !NM_IN_STRSET(ipv6_method,
                              NM_SETTING_IP6_CONFIG_METHOD_DISABLED,
@@ -12693,6 +12695,16 @@ check_and_reapply_connection(NMDevice     *self,
         if (nm_g_hash_table_lookup(diffs, NM_SETTING_IP6_CONFIG_SETTING_NAME))
             priv->ip_data_6.do_reapply = TRUE;
 
+        if (nm_g_hash_table_contains_any(
+                nm_g_hash_table_lookup(diffs, NM_SETTING_CONNECTION_SETTING_NAME),
+                NM_SETTING_CONNECTION_LLDP,
+                NM_SETTING_CONNECTION_MDNS,
+                NM_SETTING_CONNECTION_LLMNR,
+                NM_SETTING_CONNECTION_DNS_OVER_TLS)) {
+            priv->ip_data_4.do_reapply = TRUE;
+            priv->ip_data_6.do_reapply = TRUE;
+        }
+
         nm_device_activate_schedule_stage3_ip_config(self, FALSE);
 
         _routing_rules_sync(self, NM_TERNARY_TRUE);
diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h
index 80def125..d59aecb2 100644
--- a/src/core/devices/nm-device.h
+++ b/src/core/devices/nm-device.h
@@ -346,7 +346,7 @@ typedef struct _NMDeviceClass {
     NMActStageReturn (*act_stage1_prepare)(NMDevice *self, NMDeviceStateReason *out_failure_reason);
     NMActStageReturn (*act_stage2_config)(NMDevice *self, NMDeviceStateReason *out_failure_reason);
     void (*act_stage3_ip_config)(NMDevice *self, int addr_family);
-    gboolean (*ready_for_ip_config)(NMDevice *self);
+    gboolean (*ready_for_ip_config)(NMDevice *self, gboolean is_manual);
 
     const char *(*get_ip_method_auto)(NMDevice *self, int addr_family);
 
diff --git a/src/core/devices/ovs/nm-device-ovs-bridge.c b/src/core/devices/ovs/nm-device-ovs-bridge.c
index 683ada13..ea77dd18 100644
--- a/src/core/devices/ovs/nm-device-ovs-bridge.c
+++ b/src/core/devices/ovs/nm-device-ovs-bridge.c
@@ -67,7 +67,7 @@ get_generic_capabilities(NMDevice *device)
 }
 
 static gboolean
-ready_for_ip_config(NMDevice *device)
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
 {
     return FALSE;
 }
diff --git a/src/core/devices/ovs/nm-device-ovs-interface.c b/src/core/devices/ovs/nm-device-ovs-interface.c
index 6b9d3d7e..1f531a6f 100644
--- a/src/core/devices/ovs/nm-device-ovs-interface.c
+++ b/src/core/devices/ovs/nm-device-ovs-interface.c
@@ -198,7 +198,7 @@ set_platform_mtu(NMDevice *device, guint32 mtu)
 }
 
 static gboolean
-ready_for_ip_config(NMDevice *device)
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
 {
     return nm_device_get_ip_ifindex(device) > 0;
 }
diff --git a/src/core/devices/ovs/nm-device-ovs-port.c b/src/core/devices/ovs/nm-device-ovs-port.c
index 116f58c4..6ba52f40 100644
--- a/src/core/devices/ovs/nm-device-ovs-port.c
+++ b/src/core/devices/ovs/nm-device-ovs-port.c
@@ -61,7 +61,7 @@ get_generic_capabilities(NMDevice *device)
 }
 
 static gboolean
-ready_for_ip_config(NMDevice *device)
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
 {
     return FALSE;
 }
diff --git a/src/core/devices/ovs/nm-ovs-factory.c b/src/core/devices/ovs/nm-ovs-factory.c
index 3ff0abf8..5aaa5c01 100644
--- a/src/core/devices/ovs/nm-ovs-factory.c
+++ b/src/core/devices/ovs/nm-ovs-factory.c
@@ -244,7 +244,11 @@ ovsdb_interface_failed(NMOvsdb         *ovsdb,
             TRUE);
     }
 
-    nm_device_state_changed(device, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+    if (nm_device_is_activating(device)) {
+        nm_device_state_changed(device,
+                                NM_DEVICE_STATE_FAILED,
+                                NM_DEVICE_STATE_REASON_OVSDB_FAILED);
+    }
 }
 
 static void
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index d83b1f35..04608d14 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -86,6 +86,7 @@ typedef struct {
     GCancellable *scan_request_cancellable;
 
     GSource *scan_request_delay_source;
+    GSource *roam_supplicant_wait_source;
 
     NMWifiAP *current_ap;
 
@@ -939,6 +940,7 @@ deactivate(NMDevice *device)
     int                  ifindex = nm_device_get_ifindex(device);
 
     nm_clear_g_source(&priv->periodic_update_id);
+    nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
 
     cleanup_association_attempt(self, TRUE);
 
@@ -1076,8 +1078,9 @@ check_connection_compatible(NMDevice *device, NMConnection *connection, GError *
     if (s_wsec) {
         key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec);
 
-        if (nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_WEP)
-                == NM_TERNARY_FALSE
+        if (priv->sup_iface
+            && nm_supplicant_interface_get_capability(priv->sup_iface, NM_SUPPL_CAP_TYPE_WEP)
+                   == NM_TERNARY_FALSE
             && NM_IN_STRSET(key_mgmt, "ieee8021x", "none")) {
             nm_utils_error_set_literal(error,
                                        NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
@@ -2511,8 +2514,15 @@ supplicant_iface_state(NMDeviceWifi              *self,
                                                    : "Connected to wireless network",
                   (ssid_str = _nm_utils_ssid_to_string_gbytes(ssid)));
             nm_device_activate_schedule_stage3_ip_config(device, FALSE);
-        } else if (devstate == NM_DEVICE_STATE_ACTIVATED)
+        } else if (devstate == NM_DEVICE_STATE_ACTIVATED) {
             periodic_update(self);
+            if (priv->roam_supplicant_wait_source) {
+                _LOGD(LOGD_WIFI,
+                      "supplicant state settled after roaming, renew dynamic IP configuration");
+                nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
+                nm_device_update_dynamic_ip_setup(device);
+            }
+        }
         break;
     case NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED:
         if ((devstate == NM_DEVICE_STATE_ACTIVATED) || nm_device_is_activating(device)) {
@@ -2583,6 +2593,21 @@ supplicant_iface_assoc_cb(NMSupplicantInterface *iface, GError *error, gpointer
     }
 }
 
+static gboolean
+roam_supplicant_wait_timeout(gpointer user_data)
+{
+    NMDeviceWifi        *self = NM_DEVICE_WIFI(user_data);
+    NMDeviceWifiPrivate *priv = NM_DEVICE_WIFI_GET_PRIVATE(self);
+
+    _LOGD(LOGD_WIFI, "timeout waiting for supplicant to settle after roaming");
+
+    /* Eventually we still want to restart DHCP when the supplicant
+     * becomes ready */
+    nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
+    priv->roam_supplicant_wait_source = g_source_ref(nm_g_source_sentinel_get(0));
+    return G_SOURCE_CONTINUE;
+}
+
 static void
 supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
                                     GParamSpec            *pspec,
@@ -2635,7 +2660,19 @@ supplicant_iface_notify_current_bss(NMSupplicantInterface *iface,
              * Also, some APs (e.g. Cisco) can be configured to drop
              * all traffic until DHCP completes. To support such
              * cases, renew the lease when roaming to a new AP. */
-            nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+
+            if (nm_supplicant_interface_get_state(priv->sup_iface)
+                == NM_SUPPLICANT_INTERFACE_STATE_COMPLETED) {
+                nm_device_update_dynamic_ip_setup(NM_DEVICE(self));
+            } else {
+                /* Wait that the authentication to new the AP completes before
+                 * trying to renew, otherwise the DHCP REQUEST could be lost
+                 * and the client will fall back to a DISCOVER, potentially
+                 * getting a different address. */
+                nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
+                priv->roam_supplicant_wait_source =
+                    nm_g_timeout_add_source(10000, roam_supplicant_wait_timeout, self);
+            }
         }
 
         set_current_ap(self, new_ap, TRUE);
@@ -3742,6 +3779,7 @@ dispose(GObject *object)
     nm_assert(c_list_is_empty(&priv->scanning_prohibited_lst_head));
 
     nm_clear_g_source(&priv->periodic_update_id);
+    nm_clear_g_source_inst(&priv->roam_supplicant_wait_source);
 
     wifi_secrets_cancel(self);
 
diff --git a/src/core/devices/wifi/nm-iwd-manager.c b/src/core/devices/wifi/nm-iwd-manager.c
index 27222aae..2e0d51e5 100644
--- a/src/core/devices/wifi/nm-iwd-manager.c
+++ b/src/core/devices/wifi/nm-iwd-manager.c
@@ -306,7 +306,7 @@ iwd_agent_export(GDBusConnection *connection, gpointer user_data, char **agent_p
     unsigned int rnd;
     guint        id;
 
-    nm_utils_random_bytes(&rnd, sizeof(rnd));
+    nm_random_get_bytes(&rnd, sizeof(rnd));
 
     nm_sprintf_buf(path, "/agent/%u", rnd);
 
diff --git a/src/core/devices/wwan/nm-device-modem.c b/src/core/devices/wwan/nm-device-modem.c
index 77ba1b24..e0209aff 100644
--- a/src/core/devices/wwan/nm-device-modem.c
+++ b/src/core/devices/wwan/nm-device-modem.c
@@ -621,13 +621,13 @@ is_available(NMDevice *device, NMDeviceCheckDevAvailableFlags flags)
 }
 
 static gboolean
-ready_for_ip_config(NMDevice *device)
+ready_for_ip_config(NMDevice *device, gboolean is_manual)
 {
-    /* Tell NMDevice to only run device-specific IP
+    /* Tell NMDevice to only run manual and device-specific IP
      * configuration (devip) and skip other methods
-     * (manual, dhcp, etc).
+     * (dhcp, link-local, shared, etc).
      */
-    return FALSE;
+    return is_manual;
 }
 
 /*****************************************************************************/
diff --git a/src/core/devices/wwan/nm-modem-broadband.c b/src/core/devices/wwan/nm-modem-broadband.c
index cbf30f56..997fe727 100644
--- a/src/core/devices/wwan/nm-modem-broadband.c
+++ b/src/core/devices/wwan/nm-modem-broadband.c
@@ -1032,7 +1032,6 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
         l3cd = nm_l3_config_data_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                      ifindex,
                                      NM_IP_CONFIG_SOURCE_WWAN);
-        nm_l3_config_data_set_dns_priority(l3cd, AF_INET, 0);
 
         address = (NMPlatformIP4Address){
             .address      = address_network,
@@ -1116,11 +1115,9 @@ stage3_ip_config_start(NMModem *modem, int addr_family, NMModemIPMethod ip_metho
 
         _LOGI("IPv6 base configuration:");
 
-        l3cd = nm_l3_config_data_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
+        l3cd    = nm_l3_config_data_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                      ifindex,
                                      NM_IP_CONFIG_SOURCE_WWAN);
-        nm_l3_config_data_set_dns_priority(l3cd, AF_INET6, 0);
-
         do_auto = TRUE;
 
         address.plen = mm_bearer_ip_config_get_prefix(self->_priv.ipv6_config);
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 2bfd7e01..00a2d207 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -32,7 +32,10 @@
 
 /*****************************************************************************/
 
-enum { SIGNAL_NOTIFY, LAST_SIGNAL };
+enum {
+    SIGNAL_NOTIFY,
+    LAST_SIGNAL,
+};
 
 static guint signals[LAST_SIGNAL] = {0};
 
@@ -42,18 +45,30 @@ typedef struct _NMDhcpClientPrivate {
     NMDhcpClientConfig    config;
     const NML3ConfigData *l3cd;
     GSource              *no_lease_timeout_source;
-    GSource              *ipv6_lladdr_timeout_source;
+    GSource              *watch_source;
     GBytes               *effective_client_id;
-    pid_t                 pid;
-    guint                 watch_id;
-    NMDhcpState           state;
-    bool                  iaid_explicit : 1;
-    bool                  is_stopped : 1;
+
+    union {
+        struct {
+            struct {
+                GDBusMethodInvocation *invocation;
+            } bound;
+        } v4;
+        struct {
+            GSource *lladdr_timeout_source;
+            GSource *dad_timeout_source;
+        } v6;
+    };
+
     struct {
         gulong id;
         bool   wait_dhcp_commit : 1;
+        bool   wait_ipv6_dad : 1;
         bool   wait_ll_address : 1;
     } l3cfg_notify;
+
+    pid_t pid;
+    bool  is_stopped : 1;
 } NMDhcpClientPrivate;
 
 G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
@@ -62,6 +77,13 @@ G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+static gboolean _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error);
+
+_nm_unused static gboolean _dhcp_client_decline(NMDhcpClient         *self,
+                                                const NML3ConfigData *l3cd,
+                                                const char           *error_message,
+                                                GError              **error);
+
 static void
 l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self);
 
@@ -72,47 +94,76 @@ G_STATIC_ASSERT(!(((pid_t) -1) > 0));
 
 /*****************************************************************************/
 
-static void
-_emit_notify(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data)
+NM_UTILS_LOOKUP_STR_DEFINE(nm_dhcp_client_event_type_to_string,
+                           NMDhcpClientEventType,
+                           NM_UTILS_LOOKUP_DEFAULT_NM_ASSERT(NULL),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_BOUND, "bound"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE, "expire"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED, "extended"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_FAIL, "fail"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED,
+                                                    "terminated"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT, "timeout"),
+                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED,
+                                                    "unspecified"), );
+
+/*****************************************************************************/
+
+int
+nm_dhcp_client_get_addr_family(NMDhcpClient *self)
 {
-    g_signal_emit(G_OBJECT(self), signals[SIGNAL_NOTIFY], 0, notify_data);
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->config.addr_family;
 }
 
-/*****************************************************************************/
+const char *
+nm_dhcp_client_get_iface(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-static void
-connect_l3cfg_notify(NMDhcpClient *self)
+    return priv->config.iface;
+}
+
+NMDedupMultiIndex *
+nm_dhcp_client_get_multi_idx(NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    gboolean             do_connect;
 
-    do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address;
+    return nm_l3cfg_get_multi_idx(priv->config.l3cfg);
+}
 
-    if (!do_connect) {
-        nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id);
-        return;
-    }
+int
+nm_dhcp_client_get_ifindex(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    if (priv->l3cfg_notify.id == 0) {
-        priv->l3cfg_notify.id = g_signal_connect(priv->config.l3cfg,
-                                                 NM_L3CFG_SIGNAL_NOTIFY,
-                                                 G_CALLBACK(l3_cfg_notify_cb),
-                                                 self);
-    }
+    return nm_l3cfg_get_ifindex(priv->config.l3cfg);
 }
 
-pid_t
-nm_dhcp_client_get_pid(NMDhcpClient *self)
+const NMDhcpClientConfig *
+nm_dhcp_client_get_config(NMDhcpClient *self)
 {
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1);
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->pid;
+    return &priv->config;
 }
 
+GBytes *
+nm_dhcp_client_get_effective_client_id(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->effective_client_id;
+}
+
+/*****************************************************************************/
+
 void
 nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id)
 {
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    NMDhcpClientPrivate *priv    = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gs_free char        *tmp_str = NULL;
 
     g_return_if_fail(NM_IS_DHCP_CLIENT(self));
     g_return_if_fail(!client_id || g_bytes_get_size(client_id) >= 2);
@@ -123,72 +174,63 @@ nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id)
         return;
 
     g_bytes_unref(priv->effective_client_id);
-    priv->effective_client_id = client_id;
-    if (client_id)
-        g_bytes_ref(client_id);
+    priv->effective_client_id = nm_g_bytes_ref(client_id);
 
-    {
-        gs_free char *s = NULL;
+    _LOGT("%s: set %s",
+          priv->config.addr_family == AF_INET6 ? "duid" : "client-id",
+          priv->effective_client_id
+              ? (tmp_str = nm_dhcp_utils_duid_to_string(priv->effective_client_id))
+              : "default");
+}
 
-        _LOGT("%s: set %s",
-              priv->config.addr_family == AF_INET6 ? "duid" : "client-id",
-              priv->effective_client_id
-                  ? (s = nm_dhcp_utils_duid_to_string(priv->effective_client_id))
-                  : "default");
-    }
+/*****************************************************************************/
+
+static void
+_emit_notify(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data)
+{
+    g_signal_emit(G_OBJECT(self), signals[SIGNAL_NOTIFY], 0, notify_data);
 }
 
 /*****************************************************************************/
 
-NM_UTILS_LOOKUP_STR_DEFINE(nm_dhcp_state_to_string,
-                           NMDhcpState,
-                           NM_UTILS_LOOKUP_DEFAULT(NULL),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_BOUND, "bound"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_DONE, "done"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_EXPIRE, "expire"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_EXTENDED, "extended"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_FAIL, "fail"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_NOOP, "noop"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_TERMINATED, "terminated"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_TIMEOUT, "timeout"),
-                           NM_UTILS_LOOKUP_STR_ITEM(NM_DHCP_STATE_UNKNOWN, "unknown"), );
-
-static NMDhcpState
-reason_to_state(NMDhcpClient *self, const char *iface, const char *reason)
+static void
+l3_cfg_notify_check_connected(NMDhcpClient *self)
 {
-    if (g_ascii_strcasecmp(reason, "bound") == 0 || g_ascii_strcasecmp(reason, "bound6") == 0
-        || g_ascii_strcasecmp(reason, "static") == 0)
-        return NM_DHCP_STATE_BOUND;
-    else if (g_ascii_strcasecmp(reason, "renew") == 0 || g_ascii_strcasecmp(reason, "renew6") == 0
-             || g_ascii_strcasecmp(reason, "reboot") == 0
-             || g_ascii_strcasecmp(reason, "rebind") == 0
-             || g_ascii_strcasecmp(reason, "rebind6") == 0)
-        return NM_DHCP_STATE_EXTENDED;
-    else if (g_ascii_strcasecmp(reason, "timeout") == 0)
-        return NM_DHCP_STATE_TIMEOUT;
-    else if (g_ascii_strcasecmp(reason, "nak") == 0 || g_ascii_strcasecmp(reason, "expire") == 0
-             || g_ascii_strcasecmp(reason, "expire6") == 0)
-        return NM_DHCP_STATE_EXPIRE;
-    else if (g_ascii_strcasecmp(reason, "end") == 0 || g_ascii_strcasecmp(reason, "stop") == 0
-             || g_ascii_strcasecmp(reason, "stopped") == 0)
-        return NM_DHCP_STATE_DONE;
-    else if (g_ascii_strcasecmp(reason, "fail") == 0 || g_ascii_strcasecmp(reason, "abend") == 0)
-        return NM_DHCP_STATE_FAIL;
-    else if (g_ascii_strcasecmp(reason, "preinit") == 0)
-        return NM_DHCP_STATE_NOOP;
-
-    _LOGD("unmapped DHCP state '%s'", reason);
-    return NM_DHCP_STATE_UNKNOWN;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gboolean             do_connect;
+
+    do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address
+                 | priv->l3cfg_notify.wait_ipv6_dad;
+
+    if (!do_connect) {
+        nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id);
+        return;
+    }
+
+    if (priv->l3cfg_notify.id == 0) {
+        priv->l3cfg_notify.id = g_signal_connect(priv->config.l3cfg,
+                                                 NM_L3CFG_SIGNAL_NOTIFY,
+                                                 G_CALLBACK(l3_cfg_notify_cb),
+                                                 self);
+    }
 }
 
 /*****************************************************************************/
 
+pid_t
+nm_dhcp_client_get_pid(NMDhcpClient *self)
+{
+    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1);
+
+    return NM_DHCP_CLIENT_GET_PRIVATE(self)->pid;
+}
+
 static void
 watch_cleanup(NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    nm_clear_g_source(&priv->watch_id);
+    nm_clear_g_source_inst(&priv->watch_source);
 }
 
 void
@@ -225,6 +267,8 @@ stop(NMDhcpClient *self, gboolean release)
     priv->pid = -1;
 }
 
+/*****************************************************************************/
+
 static gboolean
 _no_lease_timeout(gpointer user_data)
 {
@@ -237,19 +281,12 @@ _no_lease_timeout(gpointer user_data)
                  &((NMDhcpClientNotifyData){
                      .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT,
                  }));
-    return G_SOURCE_CONTINUE;
-}
 
-const NMDhcpClientConfig *
-nm_dhcp_client_get_config(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return &priv->config;
+    return G_SOURCE_CONTINUE;
 }
 
 static void
-schedule_no_lease_timeout(NMDhcpClient *self)
+_no_lease_timeout_schedule(NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
@@ -267,29 +304,55 @@ schedule_no_lease_timeout(NMDhcpClient *self)
     }
 }
 
+/*****************************************************************************/
+
 void
-nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3ConfigData *l3cd)
+_nm_dhcp_client_notify(NMDhcpClient         *self,
+                       NMDhcpClientEventType client_event_type,
+                       const NML3ConfigData *l3cd)
 {
     NMDhcpClientPrivate                     *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     GHashTable                              *options;
     const int                                IS_IPv4     = NM_IS_IPv4(priv->config.addr_family);
     nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL;
-
-    g_return_if_fail(NM_IS_DHCP_CLIENT(self));
-
-    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) {
-        g_return_if_fail(NM_IS_L3_CONFIG_DATA(l3cd));
-        g_return_if_fail(nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family));
-    } else
-        g_return_if_fail(!l3cd);
+    char                                     sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
+
+    nm_assert(NM_IN_SET(client_event_type,
+                        NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED,
+                        NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                        NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED,
+                        NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT,
+                        NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE,
+                        NM_DHCP_CLIENT_EVENT_TYPE_FAIL,
+                        NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED));
+    nm_assert((client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT)
+              == NM_IN_SET(client_event_type,
+                           NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT,
+                           NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE,
+                           NM_DHCP_CLIENT_EVENT_TYPE_FAIL,
+                           NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED));
+    nm_assert((!!l3cd)
+              == NM_IN_SET(client_event_type,
+                           NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                           NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED));
+
+    nm_assert(!l3cd || NM_IS_L3_CONFIG_DATA(l3cd));
+    nm_assert(!l3cd || nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family));
+
+    _LOGT("notify: event=%s%s%s",
+          nm_dhcp_client_event_type_to_string(client_event_type),
+          NM_PRINT_FMT_QUOTED2(l3cd, ", l3cd=", NM_HASH_OBFUSCATE_PTR_STR(l3cd, sbuf1), ""));
 
     if (l3cd)
         nm_l3_config_data_seal(l3cd);
 
-    if (new_state >= NM_DHCP_STATE_TIMEOUT)
+    if (client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT)
         watch_cleanup(self);
 
     if (!IS_IPv4 && l3cd) {
+        /* nm_dhcp_utils_merge_new_dhcp6_lease() relies on "life_starts" option
+         * for merging, which is only set by dhclient. Internal client never sets that,
+         * but it supports multiple IP addresses per lease. */
         if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd, l3cd, &l3cd_merged)) {
             l3cd = nm_l3_config_data_seal(l3cd_merged);
         }
@@ -302,11 +365,11 @@ nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3Co
         nm_clear_g_source_inst(&priv->no_lease_timeout_source);
     } else {
         if (priv->l3cd)
-            schedule_no_lease_timeout(self);
+            _no_lease_timeout_schedule(self);
     }
 
     /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API.
-     * The internals like NMDhcpState should not be exposed (or possibly dropped in large
+     * The internals like the state should not be exposed (or possibly dropped in large
      * parts). */
 
     nm_l3_config_data_reset(&priv->l3cd, l3cd);
@@ -353,7 +416,7 @@ nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3Co
      * as a configuration parameter (in NMDhcpClientConfig). When ACD is enabled,
      * when a new lease gets announced, it must first use NML3Cfg to run ACD on the
      * interface (the previous lease -- if any -- will still be used at that point).
-     * If ACD fails, we call nm_dhcp_client_decline() and try to get a different
+     * If ACD fails, we call _dhcp_client_decline() and try to get a different
      * lease.
      * If ACD passes, we need to notify the new lease, and the user (NMDevice) may
      * then configure the address. We need to watch the configured addresses (in NML3Cfg),
@@ -367,13 +430,24 @@ nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3Co
      * as a static address (bypassing ACD), then NML3Cfg is aware of that and signals
      * immediate success. */
 
-    if (nm_dhcp_client_can_accept(self) && new_state == NM_DHCP_STATE_BOUND && priv->l3cd
+    if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd
         && nm_l3_config_data_get_num_addresses(priv->l3cd, priv->config.addr_family) > 0) {
         priv->l3cfg_notify.wait_dhcp_commit = TRUE;
     } else {
         priv->l3cfg_notify.wait_dhcp_commit = FALSE;
     }
-    connect_l3cfg_notify(self);
+
+    if (!priv->l3cfg_notify.wait_dhcp_commit && priv->l3cd) {
+        gs_free_error GError *error = NULL;
+
+        _LOGD("accept lease right away");
+        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
+            _LOGD("accept failed: %s", error->message);
+            /* Unclear why this happened, or what to do about it. Just proceed. */
+        }
+    }
+
+    l3_cfg_notify_check_connected(self);
 
     {
         const NMDhcpClientNotifyData notify_data = {
@@ -396,14 +470,15 @@ daemon_watch_cb(GPid pid, int status, gpointer user_data)
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     gs_free char        *desc = NULL;
 
-    g_return_if_fail(priv->watch_id);
-    priv->watch_id = 0;
+    g_return_if_fail(priv->watch_source);
+
+    priv->watch_source = NULL;
 
     _LOGI("client pid %d %s", pid, (desc = nm_utils_get_process_exit_status_desc(status)));
 
     priv->pid = -1;
 
-    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL);
+    _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED, NULL);
 }
 
 void
@@ -414,8 +489,8 @@ nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid)
     g_return_if_fail(priv->pid == -1);
     priv->pid = pid;
 
-    g_return_if_fail(priv->watch_id == 0);
-    priv->watch_id = g_child_watch_add(pid, daemon_watch_cb, self);
+    g_return_if_fail(!priv->watch_source);
+    priv->watch_source = nm_g_child_watch_add_source(pid, daemon_watch_cb, self);
 }
 
 void
@@ -429,71 +504,74 @@ nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid)
     watch_cleanup(self);
 }
 
-gboolean
-nm_dhcp_client_start_ip4(NMDhcpClient *self, GError **error)
+static gboolean
+_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error)
 {
-    NMDhcpClientPrivate *priv;
-
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    g_return_val_if_fail(priv->pid == -1, FALSE);
-    g_return_val_if_fail(priv->config.addr_family == AF_INET, FALSE);
-    g_return_val_if_fail(priv->config.uuid, FALSE);
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return TRUE;
 
-    schedule_no_lease_timeout(self);
+    if (!priv->v4.bound.invocation)
+        return TRUE;
 
-    return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, error);
+    g_dbus_method_invocation_return_value(g_steal_pointer(&priv->v4.bound.invocation), NULL);
+    return TRUE;
 }
 
-gboolean
-nm_dhcp_client_accept(NMDhcpClient *self, GError **error)
+static gboolean
+_dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error)
 {
-    NMDhcpClientPrivate *priv;
+    NMDhcpClientClass *klass;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+    nm_assert(l3cd);
 
-    priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    g_return_val_if_fail(priv->l3cd, FALSE);
+    klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    if (NM_DHCP_CLIENT_GET_CLASS(self)->accept) {
-        return NM_DHCP_CLIENT_GET_CLASS(self)->accept(self, error);
-    }
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
 
-    return TRUE;
+    return klass->accept(self, l3cd, error);
 }
 
-gboolean
-nm_dhcp_client_can_accept(NMDhcpClient *self)
+static gboolean
+decline(NMDhcpClient *self, const NML3ConfigData *l3cd, const char *error_message, GError **error)
 {
-    gboolean can_accept;
-
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    can_accept = !!(NM_DHCP_CLIENT_GET_CLASS(self)->accept);
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return TRUE;
 
-    nm_assert(can_accept == (!!(NM_DHCP_CLIENT_GET_CLASS(self)->decline)));
+    if (!priv->v4.bound.invocation) {
+        nm_utils_error_set(error,
+                           NM_UTILS_ERROR_UNKNOWN,
+                           "calling decline in unexpected script state");
+        return FALSE;
+    }
 
-    return can_accept;
+    g_dbus_method_invocation_return_error(g_steal_pointer(&priv->v4.bound.invocation),
+                                          NM_DEVICE_ERROR,
+                                          NM_DEVICE_ERROR_FAILED,
+                                          "acd failed");
+    return TRUE;
 }
 
-gboolean
-nm_dhcp_client_decline(NMDhcpClient *self, const char *error_message, GError **error)
+static gboolean
+_dhcp_client_decline(NMDhcpClient         *self,
+                     const NML3ConfigData *l3cd,
+                     const char           *error_message,
+                     GError              **error)
 {
-    NMDhcpClientPrivate *priv;
+    NMDhcpClientClass *klass;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+    nm_assert(l3cd);
 
-    priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    g_return_val_if_fail(priv->l3cd, FALSE);
+    klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    if (NM_DHCP_CLIENT_GET_CLASS(self)->decline) {
-        return NM_DHCP_CLIENT_GET_CLASS(self)->decline(self, error_message, error);
-    }
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
 
-    return TRUE;
+    return klass->decline(self, l3cd, error_message, error);
 }
 
 static GBytes *
@@ -508,7 +586,7 @@ ipv6_lladdr_timeout(gpointer user_data)
     NMDhcpClient        *self = user_data;
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
+    nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source);
 
     _emit_notify(
         self,
@@ -519,6 +597,23 @@ ipv6_lladdr_timeout(gpointer user_data)
     return G_SOURCE_CONTINUE;
 }
 
+static gboolean
+ipv6_dad_timeout(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
+
+    _emit_notify(
+        self,
+        &((NMDhcpClientNotifyData){
+            .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+            .it_looks_bad.reason = "timeout reached while waiting for IPv6 DAD to complete",
+        }));
+    return G_SOURCE_CONTINUE;
+}
+
 static const NMPlatformIP6Address *
 ipv6_lladdr_find(NMDhcpClient *self)
 {
@@ -528,6 +623,8 @@ ipv6_lladdr_find(NMDhcpClient *self)
     NMDedupMultiIter     iter;
     const NMPObject     *obj;
 
+    nm_assert(!NM_IS_IPv4(priv->config.addr_family));
+
     l3cfg = priv->config.l3cfg;
     nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_IP6_ADDRESS, nm_l3cfg_get_ifindex(l3cfg));
 
@@ -544,6 +641,37 @@ ipv6_lladdr_find(NMDhcpClient *self)
     return NULL;
 }
 
+static const NMPlatformIP6Address *
+ipv6_tentative_addr_find(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate        *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    NMDedupMultiIter            iter;
+    const NMPlatformIP6Address *addr;
+    NML3Cfg                    *l3cfg = priv->config.l3cfg;
+
+    /* For each address in the lease, check whether it's tentative
+     * in platform. */
+    nm_l3_config_data_iter_ip6_address_for_each (&iter, priv->l3cd, &addr) {
+        const NMPlatformIP6Address *pladdr;
+        NMPObject                   needle;
+
+        nmp_object_stackinit_id_ip6_address(&needle, nm_l3cfg_get_ifindex(l3cfg), &addr->address);
+        pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(nm_platform_lookup_obj(nm_l3cfg_get_platform(l3cfg),
+                                                                    NMP_CACHE_ID_TYPE_OBJECT_TYPE,
+                                                                    &needle));
+        if (!pladdr) {
+            /* Address was removed from platform */
+            continue;
+        }
+
+        if (NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_TENTATIVE)
+            && !NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_OPTIMISTIC))
+            return pladdr;
+    }
+
+    return NULL;
+}
+
 static void
 l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self)
 {
@@ -551,23 +679,19 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
 
     nm_assert(l3cfg == priv->config.l3cfg);
 
-    switch (notify_data->notify_type) {
-    case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
-    {
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE
+        && priv->l3cfg_notify.wait_ll_address) {
         const NMPlatformIP6Address *addr;
         gs_free_error GError       *error = NULL;
 
-        if (!priv->l3cfg_notify.wait_ll_address)
-            return;
-
         addr = ipv6_lladdr_find(self);
         if (addr) {
             _LOGD("got IPv6LL address, starting transaction");
             priv->l3cfg_notify.wait_ll_address = FALSE;
-            connect_l3cfg_notify(self);
-            nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
+            l3_cfg_notify_check_connected(self);
+            nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source);
 
-            schedule_no_lease_timeout(self);
+            _no_lease_timeout_schedule(self);
 
             if (!NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, &error)) {
                 _emit_notify(self,
@@ -577,11 +701,30 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                              }));
             }
         }
+    }
 
-        break;
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE
+        && priv->l3cfg_notify.wait_ipv6_dad) {
+        const NMPlatformIP6Address *tentative;
+
+        tentative = ipv6_tentative_addr_find(self);
+        if (!tentative) {
+            _LOGD("addresses in the lease completed DAD");
+            priv->l3cfg_notify.wait_ipv6_dad = FALSE;
+            nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
+            l3_cfg_notify_check_connected(self);
+            _emit_notify(
+                self,
+                &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                                           .lease_update = {
+                                               .l3cd     = priv->l3cd,
+                                               .accepted = TRUE,
+                                           }}));
+        }
     }
-    case NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT:
-    {
+
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT
+        && priv->l3cfg_notify.wait_dhcp_commit) {
         const NML3ConfigData      *committed_l3cd;
         NMDedupMultiIter           ipconf_iter;
         const NMPlatformIPAddress *lease_address;
@@ -592,9 +735,6 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
          * configured. If the address was added, we can proceed accepting the
          * lease and notifying NMDevice. */
 
-        if (!priv->l3cfg_notify.wait_dhcp_commit)
-            return;
-
         nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter,
                                                     priv->l3cd,
                                                     priv->config.addr_family,
@@ -610,20 +750,35 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                                                     address4->address,
                                                     address4->plen,
                                                     address4->peer_address))
-                return;
+                goto wait_dhcp_commit_done;
         } else {
             const NMPlatformIP6Address *address6 = (const NMPlatformIP6Address *) lease_address;
+            const NMPlatformIP6Address *tentative;
+            char                        str[NM_UTILS_TO_STRING_BUFFER_SIZE];
 
             if (!nm_l3_config_data_lookup_address_6(committed_l3cd, &address6->address))
-                return;
+                goto wait_dhcp_commit_done;
+
+            tentative = ipv6_tentative_addr_find(self);
+            if (tentative) {
+                priv->l3cfg_notify.wait_ipv6_dad = TRUE;
+                priv->v6.dad_timeout_source =
+                    nm_g_timeout_add_seconds_source(30, ipv6_dad_timeout, self);
+                _LOGD("wait DAD for address %s",
+                      nm_platform_ip6_address_to_string(tentative, str, sizeof(str)));
+            } else {
+                priv->l3cfg_notify.wait_ipv6_dad = FALSE;
+                nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
+            }
         }
 
         priv->l3cfg_notify.wait_dhcp_commit = FALSE;
-        connect_l3cfg_notify(self);
 
-        _LOGD("accept address");
+        l3_cfg_notify_check_connected(self);
 
-        if (!nm_dhcp_client_accept(self, &error)) {
+        _LOGD("accept lease");
+
+        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
             gs_free char *reason = g_strdup_printf("error accepting lease: %s", error->message);
 
             _emit_notify(self,
@@ -631,58 +786,67 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                              .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
                              .it_looks_bad.reason = reason,
                          }));
-            return;
+            goto wait_dhcp_commit_done;
         }
 
-        _emit_notify(
-            self,
-            &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
-                                       .lease_update = {
-                                           .l3cd     = priv->l3cd,
-                                           .accepted = TRUE,
-                                       }}));
-        break;
-    };
-    default:
-        /* ignore */;
+        if (priv->config.addr_family == AF_INET || !priv->l3cfg_notify.wait_ipv6_dad) {
+            _emit_notify(
+                self,
+                &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                                           .lease_update = {
+                                               .l3cd     = priv->l3cd,
+                                               .accepted = TRUE,
+                                           }}));
+        }
     }
+wait_dhcp_commit_done:;
 }
 
 gboolean
-nm_dhcp_client_start_ip6(NMDhcpClient *self, GError **error)
+nm_dhcp_client_start(NMDhcpClient *self, GError **error)
 {
     NMDhcpClientPrivate        *priv;
     gs_unref_bytes GBytes      *own_client_id = NULL;
-    const NMPlatformIP6Address *addr;
+    const NMPlatformIP6Address *addr          = NULL;
+    int                         IS_IPv4;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
     g_return_val_if_fail(priv->pid == -1, FALSE);
-    g_return_val_if_fail(priv->config.addr_family == AF_INET6, FALSE);
     g_return_val_if_fail(priv->config.uuid, FALSE);
-    g_return_val_if_fail(!priv->effective_client_id, FALSE);
+    nm_assert(!priv->effective_client_id);
 
-    if (!priv->config.v6.enforce_duid)
-        own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self);
+    IS_IPv4 = NM_IS_IPv4(priv->config.addr_family);
 
-    nm_dhcp_client_set_effective_client_id(self, own_client_id ?: priv->config.client_id);
+    if (!IS_IPv4) {
+        if (!priv->config.v6.enforce_duid)
+            own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self);
 
-    addr = ipv6_lladdr_find(self);
-    if (!addr) {
-        _LOGD("waiting for IPv6LL address");
-        priv->l3cfg_notify.wait_ll_address = TRUE;
-        connect_l3cfg_notify(self);
-        priv->ipv6_lladdr_timeout_source =
-            nm_g_timeout_add_seconds_source(10, ipv6_lladdr_timeout, self);
-        return TRUE;
+        nm_dhcp_client_set_effective_client_id(self, own_client_id ?: priv->config.client_id);
+
+        addr = ipv6_lladdr_find(self);
+        if (!addr) {
+            _LOGD("waiting for IPv6LL address");
+            priv->l3cfg_notify.wait_ll_address = TRUE;
+            l3_cfg_notify_check_connected(self);
+            priv->v6.lladdr_timeout_source =
+                nm_g_timeout_add_seconds_source(10, ipv6_lladdr_timeout, self);
+            return TRUE;
+        }
     }
 
-    schedule_no_lease_timeout(self);
+    _no_lease_timeout_schedule(self);
+
+    if (IS_IPv4)
+        return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, error);
 
     return NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, error);
 }
 
+/*****************************************************************************/
+
 void
 nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name)
 {
@@ -757,9 +921,17 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
 
     priv->is_stopped = TRUE;
 
+    if (NM_IS_IPv4(priv->config.addr_family) && priv->v4.bound.invocation) {
+        g_dbus_method_invocation_return_error(g_steal_pointer(&priv->v4.bound.invocation),
+                                              NM_DEVICE_ERROR,
+                                              NM_DEVICE_ERROR_FAILED,
+                                              "dhcp stopping");
+    }
+
     priv->l3cfg_notify.wait_dhcp_commit = FALSE;
     priv->l3cfg_notify.wait_ll_address  = FALSE;
-    connect_l3cfg_notify(self);
+    priv->l3cfg_notify.wait_ipv6_dad    = FALSE;
+    l3_cfg_notify_check_connected(self);
 
     /* Kill the DHCP client */
     old_pid = priv->pid;
@@ -770,7 +942,7 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
         _LOGI("canceled DHCP transaction");
     nm_assert(priv->pid == -1);
 
-    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL);
+    _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED, NULL);
 }
 
 /*****************************************************************************/
@@ -895,16 +1067,17 @@ nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient *self, const NMPlatformIP
 }
 
 gboolean
-nm_dhcp_client_handle_event(gpointer      unused,
-                            const char   *iface,
-                            int           pid,
-                            GVariant     *options,
-                            const char   *reason,
-                            NMDhcpClient *self)
+nm_dhcp_client_handle_event(gpointer               unused,
+                            const char            *iface,
+                            int                    pid,
+                            GVariant              *options,
+                            const char            *reason,
+                            GDBusMethodInvocation *invocation,
+                            NMDhcpClient          *self)
 {
     NMDhcpClientPrivate                    *priv;
-    guint32                                 new_state;
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
+    NMDhcpClientEventType                   client_event_type;
     NMPlatformIP6Address                    prefix = {
         0,
     };
@@ -914,6 +1087,7 @@ nm_dhcp_client_handle_event(gpointer      unused,
     g_return_val_if_fail(pid > 0, FALSE);
     g_return_val_if_fail(g_variant_is_of_type(options, G_VARIANT_TYPE_VARDICT), FALSE);
     g_return_val_if_fail(reason != NULL, FALSE);
+    g_return_val_if_fail(G_IS_DBUS_METHOD_INVOCATION(invocation), FALSE);
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
@@ -922,17 +1096,29 @@ nm_dhcp_client_handle_event(gpointer      unused,
     if (priv->pid != pid)
         return FALSE;
 
-    new_state = reason_to_state(self, priv->config.iface, reason);
-    if (new_state == NM_DHCP_STATE_NOOP)
-        return TRUE;
-
-    _LOGD("DHCP state '%s' -> '%s' (reason: '%s')",
-          nm_dhcp_state_to_string(priv->state),
-          nm_dhcp_state_to_string(new_state),
-          reason);
-    priv->state = new_state;
+    _LOGD("DHCP event (reason: '%s')", reason);
+
+    if (NM_IN_STRSET_ASCII_CASE(reason, "preinit"))
+        goto out_handled;
+
+    if (NM_IN_STRSET_ASCII_CASE(reason, "bound", "bound6", "static"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_BOUND;
+    else if (NM_IN_STRSET_ASCII_CASE(reason, "renew", "renew6", "reboot", "rebind", "rebind6"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED;
+    else if (NM_IN_STRSET_ASCII_CASE(reason, "timeout"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT;
+    else if (NM_IN_STRSET_ASCII_CASE(reason, "nak", "expire", "expire6"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE;
+    else if (NM_IN_STRSET_ASCII_CASE(reason, "end", "stop", "stopped"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED;
+    else if (NM_IN_STRSET_ASCII_CASE(reason, "fail", "abend"))
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_FAIL;
+    else
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED;
 
-    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) {
+    if (NM_IN_SET(client_event_type,
+                  NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                  NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED)) {
         gs_unref_hashtable GHashTable *str_options = NULL;
         GVariantIter                   iter;
         const char                    *name;
@@ -963,8 +1149,7 @@ nm_dhcp_client_handle_event(gpointer      unused,
                     str_options,
                     priv->config.v6.info_only);
             }
-        } else
-            g_warn_if_reached();
+        }
 
         if (l3cd) {
             nm_l3_config_data_set_dhcp_lease_from_options(l3cd,
@@ -978,16 +1163,32 @@ nm_dhcp_client_handle_event(gpointer      unused,
          * of the DHCP client instance. Instead, we just signal the prefix
          * to the device. */
         nm_dhcp_client_emit_ipv6_prefix_delegated(self, &prefix);
-        return TRUE;
+        goto out_handled;
     }
 
-    /* Fail if no valid IP config was received */
-    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED) && !l3cd) {
+    if (NM_IN_SET(client_event_type,
+                  NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                  NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED)
+        && !l3cd) {
+        /* Fail if no valid IP config was received */
         _LOGW("client bound but IP config not received");
-        new_state = NM_DHCP_STATE_FAIL;
+        client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_FAIL;
     }
 
-    nm_dhcp_client_set_state(self, new_state, l3cd);
+    if (priv->v4.bound.invocation)
+        g_dbus_method_invocation_return_value(g_steal_pointer(&priv->v4.bound.invocation), NULL);
+
+    if (NM_IS_IPv4(priv->config.addr_family)
+        && NM_IN_SET(client_event_type,
+                     NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                     NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED))
+        priv->v4.bound.invocation = g_steal_pointer(&invocation);
+
+    _nm_dhcp_client_notify(self, client_event_type, l3cd);
+
+out_handled:
+    if (invocation)
+        g_dbus_method_invocation_return_value(invocation, NULL);
     return TRUE;
 }
 
@@ -1023,21 +1224,26 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
     return FALSE;
 }
 
+/*****************************************************************************/
+
 static void
 config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
 {
+    nm_assert(config);
+    nm_assert(src);
+    nm_assert(config != src);
+    nm_assert_addr_family(src->addr_family);
+
     *config = *src;
 
+    /* We must not return before un-aliasing all pointers in @config! */
+
     g_object_ref(config->l3cfg);
 
-    if (config->hwaddr)
-        g_bytes_ref(config->hwaddr);
-    if (config->bcast_hwaddr)
-        g_bytes_ref(config->bcast_hwaddr);
-    if (config->vendor_class_identifier)
-        g_bytes_ref(config->vendor_class_identifier);
-    if (config->client_id)
-        g_bytes_ref(config->client_id);
+    nm_g_bytes_ref(config->hwaddr);
+    nm_g_bytes_ref(config->bcast_hwaddr);
+    nm_g_bytes_ref(config->vendor_class_identifier);
+    nm_g_bytes_ref(config->client_id);
 
     config->iface           = g_strdup(config->iface);
     config->uuid            = g_strdup(config->uuid);
@@ -1047,14 +1253,12 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
 
     config->reject_servers = (const char *const *) nm_strv_dup(config->reject_servers, -1, TRUE);
 
-    if (config->addr_family == AF_INET) {
+    if (NM_IS_IPv4(config->addr_family))
         config->v4.last_address = g_strdup(config->v4.last_address);
-    } else if (config->addr_family == AF_INET6) {
+    else {
         config->hwaddr       = NULL;
         config->bcast_hwaddr = NULL;
         config->use_fqdn     = TRUE;
-    } else {
-        nm_assert_not_reached();
     }
 
     if (!config->hostname && config->send_hostname) {
@@ -1114,46 +1318,6 @@ config_clear(NMDhcpClientConfig *config)
     }
 }
 
-int
-nm_dhcp_client_get_addr_family(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return priv->config.addr_family;
-}
-
-const char *
-nm_dhcp_client_get_iface(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return priv->config.iface;
-}
-
-NMDedupMultiIndex *
-nm_dhcp_client_get_multi_idx(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return nm_l3cfg_get_multi_idx(priv->config.l3cfg);
-}
-
-int
-nm_dhcp_client_get_ifindex(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return nm_l3cfg_get_ifindex(priv->config.l3cfg);
-}
-
-GBytes *
-nm_dhcp_client_get_effective_client_id(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    return priv->effective_client_id;
-}
-
 /*****************************************************************************/
 
 static void
@@ -1165,6 +1329,21 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
     case PROP_CONFIG:
         /* construct-only */
         config_init(&priv->config, g_value_get_pointer(value));
+
+        /* I know, this is technically not necessary. It just feels nicer to
+         * explicitly initialize the respective union member. */
+        if (NM_IS_IPv4(priv->config.addr_family)) {
+            priv->v4 = (typeof(priv->v4)){
+                .bound =
+                    {
+                        .invocation = NULL,
+                    },
+            };
+        } else {
+            priv->v6 = (typeof(priv->v6)){
+                .lladdr_timeout_source = NULL,
+            };
+        }
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
@@ -1196,7 +1375,12 @@ dispose(GObject *object)
     watch_cleanup(self);
 
     nm_clear_g_source_inst(&priv->no_lease_timeout_source);
-    nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
+
+    if (!NM_IS_IPv4(priv->config.addr_family)) {
+        nm_clear_g_source_inst(&priv->v6.lladdr_timeout_source);
+        nm_clear_g_source_inst(&priv->v6.dad_timeout_source);
+    }
+
     nm_clear_pointer(&priv->effective_client_id, g_bytes_unref);
 
     G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object);
@@ -1223,6 +1407,8 @@ nm_dhcp_client_class_init(NMDhcpClientClass *client_class)
     object_class->dispose      = dispose;
     object_class->finalize     = finalize;
     object_class->set_property = set_property;
+    client_class->accept       = _accept;
+    client_class->decline      = decline;
 
     client_class->stop     = stop;
     client_class->get_duid = get_duid;
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index 249bd013..e4b99929 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -27,16 +27,16 @@
 #define NM_DHCP_CLIENT_NOTIFY "dhcp-notify"
 
 typedef enum {
-    NM_DHCP_STATE_UNKNOWN = 0,
-    NM_DHCP_STATE_BOUND,      /* new lease */
-    NM_DHCP_STATE_EXTENDED,   /* lease extended */
-    NM_DHCP_STATE_TIMEOUT,    /* timed out contacting server */
-    NM_DHCP_STATE_DONE,       /* client reported it's stopping */
-    NM_DHCP_STATE_EXPIRE,     /* lease expired or NAKed */
-    NM_DHCP_STATE_FAIL,       /* failed for some reason */
-    NM_DHCP_STATE_TERMINATED, /* client is no longer running */
-    NM_DHCP_STATE_NOOP,       /* state is a non operation for NetworkManager */
-} NMDhcpState;
+    NM_DHCP_CLIENT_EVENT_TYPE_UNSPECIFIED,
+
+    NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+    NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED,
+
+    NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT,
+    NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE,
+    NM_DHCP_CLIENT_EVENT_TYPE_FAIL,
+    NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED,
+} NMDhcpClientEventType;
 
 typedef enum _nm_packed {
     NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
@@ -82,17 +82,8 @@ typedef struct {
     };
 } NMDhcpClientNotifyData;
 
-const char *nm_dhcp_state_to_string(NMDhcpState state);
-
-/* FIXME(l3cfg:dhcp:config): nm_dhcp_manager_start_ip[46]() has a gazillion of parameters,
- * those get passed on as CONSTRUCT_ONLY properties to the NMDhcpClient. Drop
- * all these parameters, and let the caller provide one NMDhcpClientConfig
- * instance. There will be only one GObject property (NM_DHCP_CLIENT_CONFIG),
- * which is CONSTRUCT_ONLY and takes a (mandatory) G_TYPE_POINTER for the
- * configuration.
- *
- * Since NMDhcpClientConfig has an addr_family, we also don't need separate
- * nm_dhcp_manager_start_ip[46]() methods. */
+const char *nm_dhcp_client_event_type_to_string(NMDhcpClientEventType client_event_type);
+
 typedef struct {
     int addr_family;
 
@@ -156,12 +147,13 @@ typedef struct {
 
     union {
         struct {
+            /* The address from the previous lease */
+            const char *last_address;
+
             /* Set BOOTP broadcast flag in request packets, so that servers
              * will always broadcast replies. */
             bool request_broadcast : 1;
 
-            /* The address from the previous lease */
-            const char *last_address;
         } v4;
         struct {
             /* If set, the DUID from the connection is used; otherwise
@@ -208,9 +200,12 @@ typedef struct {
 
     gboolean (*ip4_start)(NMDhcpClient *self, GError **error);
 
-    gboolean (*accept)(NMDhcpClient *self, GError **error);
+    gboolean (*accept)(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **error);
 
-    gboolean (*decline)(NMDhcpClient *self, const char *error_message, GError **error);
+    gboolean (*decline)(NMDhcpClient         *self,
+                        const NML3ConfigData *l3cd,
+                        const char           *error_message,
+                        GError              **error);
 
     gboolean (*ip6_start)(NMDhcpClient *self, const struct in6_addr *ll_addr, GError **error);
 
@@ -230,8 +225,7 @@ typedef struct {
 
 GType nm_dhcp_client_get_type(void);
 
-gboolean nm_dhcp_client_start_ip4(NMDhcpClient *self, GError **error);
-gboolean nm_dhcp_client_start_ip6(NMDhcpClient *self, GError **error);
+gboolean nm_dhcp_client_start(NMDhcpClient *self, GError **error);
 
 const NMDhcpClientConfig *nm_dhcp_client_get_config(NMDhcpClient *self);
 
@@ -250,11 +244,6 @@ nm_dhcp_client_get_lease(NMDhcpClient *self)
     return NULL;
 }
 
-gboolean nm_dhcp_client_accept(NMDhcpClient *self, GError **error);
-gboolean nm_dhcp_client_can_accept(NMDhcpClient *self);
-
-gboolean nm_dhcp_client_decline(NMDhcpClient *self, const char *error_message, GError **error);
-
 void nm_dhcp_client_stop(NMDhcpClient *self, gboolean release);
 
 /* Backend helpers for subclasses */
@@ -268,15 +257,17 @@ void nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid);
 
 void nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid);
 
-void
-nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3ConfigData *l3cd);
-
-gboolean nm_dhcp_client_handle_event(gpointer      unused,
-                                     const char   *iface,
-                                     int           pid,
-                                     GVariant     *options,
-                                     const char   *reason,
-                                     NMDhcpClient *self);
+void _nm_dhcp_client_notify(NMDhcpClient         *self,
+                            NMDhcpClientEventType client_event_type,
+                            const NML3ConfigData *l3cd);
+
+gboolean nm_dhcp_client_handle_event(gpointer               unused,
+                                     const char            *iface,
+                                     int                    pid,
+                                     GVariant              *options,
+                                     const char            *reason,
+                                     GDBusMethodInvocation *invocation,
+                                     NMDhcpClient          *self);
 
 void nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient               *self,
                                                const NMPlatformIP6Address *prefix);
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index 41862f2b..aab658a2 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -100,21 +100,6 @@ next:;
     return g_variant_ref_sink(g_variant_new("(a{sv})", &builder));
 }
 
-static void
-kill_pid(void)
-{
-    const char *pid_str;
-    pid_t       pid = 0;
-
-    pid_str = getenv("pid");
-    if (pid_str)
-        pid = strtol(pid_str, NULL, 10);
-    if (pid) {
-        _LOGI("a fatal error occurred, kill dhclient instance with pid %d", pid);
-        kill(pid, SIGTERM);
-    }
-}
-
 int
 main(int argc, char *argv[])
 {
@@ -180,7 +165,7 @@ do_notify:
                                          parameters,
                                          NULL,
                                          G_DBUS_CALL_FLAGS_NONE,
-                                         1000,
+                                         60000,
                                          NULL,
                                          &error);
 
@@ -236,7 +221,5 @@ do_notify:
     }
 
 out:
-    if (!success)
-        kill_pid();
     return success ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff --git a/src/core/dhcp/nm-dhcp-listener.c b/src/core/dhcp/nm-dhcp-listener.c
index 2c567593..0854c1dc 100644
--- a/src/core/dhcp/nm-dhcp-listener.c
+++ b/src/core/dhcp/nm-dhcp-listener.c
@@ -128,7 +128,7 @@ get_option(GVariant *options, const char *key)
 }
 
 static void
-_method_call_handle(NMDhcpListener *self, GVariant *parameters)
+_method_call_handle(NMDhcpListener *self, GDBusMethodInvocation *invocation, GVariant *parameters)
 {
     gs_free char              *iface   = NULL;
     gs_free char              *pid_str = NULL;
@@ -142,23 +142,23 @@ _method_call_handle(NMDhcpListener *self, GVariant *parameters)
     iface = get_option(options, "interface");
     if (iface == NULL) {
         _LOGW("dhcp-event: didn't have associated interface.");
-        return;
+        goto out;
     }
 
     pid_str = get_option(options, "pid");
     pid     = _nm_utils_ascii_str_to_int64(pid_str, 10, 0, G_MAXINT32, -1);
     if (pid == -1) {
         _LOGW("dhcp-event: couldn't convert PID '%s' to an integer", pid_str ?: "(null)");
-        return;
+        goto out;
     }
 
     reason = get_option(options, "reason");
     if (reason == NULL) {
         _LOGW("dhcp-event: (pid %d) DHCP event didn't have a reason", pid);
-        return;
+        goto out;
     }
 
-    g_signal_emit(self, signals[EVENT], 0, iface, pid, options, reason, &handled);
+    g_signal_emit(self, signals[EVENT], 0, iface, pid, options, reason, invocation, &handled);
     if (!handled) {
         if (g_ascii_strcasecmp(reason, "RELEASE") == 0) {
             /* Ignore event when the dhcp client gets killed and we receive its last message */
@@ -166,6 +166,10 @@ _method_call_handle(NMDhcpListener *self, GVariant *parameters)
         } else
             _LOGW("dhcp-event: (pid %d) unhandled DHCP event for interface %s", pid, iface);
     }
+
+out:
+    if (!handled)
+        g_dbus_method_invocation_return_value(invocation, NULL);
 }
 
 static void
@@ -190,8 +194,7 @@ _method_call(GDBusConnection       *connection,
         return;
     }
 
-    _method_call_handle(self, parameters);
-    g_dbus_method_invocation_return_value(invocation, NULL);
+    _method_call_handle(self, invocation, parameters);
 }
 
 static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
@@ -311,9 +314,10 @@ nm_dhcp_listener_class_init(NMDhcpListenerClass *listener_class)
                                   NULL,
                                   NULL,
                                   G_TYPE_BOOLEAN, /* listeners return TRUE if handled */
-                                  4,
+                                  5,
                                   G_TYPE_STRING,  /* iface */
                                   G_TYPE_INT,     /* pid */
                                   G_TYPE_VARIANT, /* options */
-                                  G_TYPE_STRING); /* reason */
+                                  G_TYPE_STRING,  /* reason */
+                                  G_TYPE_DBUS_METHOD_INVOCATION /* invocation*/);
 }
diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c
index f353e637..9fea1666 100644
--- a/src/core/dhcp/nm-dhcp-manager.c
+++ b/src/core/dhcp/nm-dhcp-manager.c
@@ -131,8 +131,7 @@ NMDhcpClient *
 nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GError **error)
 {
     NMDhcpManagerPrivate         *priv;
-    gs_unref_object NMDhcpClient *client  = NULL;
-    gboolean                      success = FALSE;
+    gs_unref_object NMDhcpClient *client = NULL;
     gsize                         hwaddr_len;
     GType                         gtype;
 
@@ -202,13 +201,7 @@ nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GE
      * default outside of NetworkManager API.
      */
 
-    if (config->addr_family == AF_INET) {
-        success = nm_dhcp_client_start_ip4(client, error);
-    } else {
-        success = nm_dhcp_client_start_ip6(client, error);
-    }
-
-    if (!success)
+    if (!nm_dhcp_client_start(client, error))
         return NULL;
 
     return g_steal_pointer(&client);
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index aac18967..2e41cf15 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -50,9 +50,14 @@ typedef struct _NMDhcpNettoolsClass NMDhcpNettoolsClass;
 typedef struct {
     NDhcp4Client      *client;
     NDhcp4ClientProbe *probe;
-    NDhcp4ClientLease *lease;
-    GSource           *event_source;
-    char              *lease_file;
+
+    struct {
+        NDhcp4ClientLease    *lease;
+        const NML3ConfigData *lease_l3cd;
+    } granted;
+
+    GSource *event_source;
+    char    *lease_file;
 } NMDhcpNettoolsPrivate;
 
 struct _NMDhcpNettools {
@@ -778,15 +783,19 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
 }
 
 static void
-bound4_handle(NMDhcpNettools *self, NDhcp4ClientLease *lease, gboolean extended)
+bound4_handle(NMDhcpNettools *self, guint event, NDhcp4ClientLease *lease)
 {
     NMDhcpNettoolsPrivate                  *priv   = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     NMDhcpClient                           *client = NM_DHCP_CLIENT(self);
     const NMDhcpClientConfig               *client_config;
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
-    GError                                 *error = NULL;
+    gs_free_error GError                   *error = NULL;
+
+    nm_assert(NM_IN_SET(event, N_DHCP4_CLIENT_EVENT_GRANTED, N_DHCP4_CLIENT_EVENT_EXTENDED));
+    nm_assert(lease);
+
+    _LOGT("lease available (%s)", (event == N_DHCP4_CLIENT_EVENT_GRANTED) ? "granted" : "extended");
 
-    _LOGT("lease available (%s)", extended ? "extended" : "new");
     client_config = nm_dhcp_client_get_config(client);
     l3cd          = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(client),
                                client_config->iface,
@@ -795,29 +804,51 @@ bound4_handle(NMDhcpNettools *self, NDhcp4ClientLease *lease, gboolean extended)
                                &error);
     if (!l3cd) {
         _LOGW("failure to parse lease: %s", error->message);
-        g_clear_error(&error);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+
+        if (event == N_DHCP4_CLIENT_EVENT_GRANTED)
+            n_dhcp4_client_lease_decline(lease, "invalid lease");
+
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
     }
 
-    lease_save(self, lease, priv->lease_file);
-
-    nm_dhcp_client_set_state(NM_DHCP_CLIENT(self),
-                             extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND,
-                             l3cd);
+    if (event == N_DHCP4_CLIENT_EVENT_GRANTED) {
+        priv->granted.lease      = n_dhcp4_client_lease_ref(lease);
+        priv->granted.lease_l3cd = nm_l3_config_data_ref(l3cd);
+    } else
+        lease_save(self, lease, priv->lease_file);
+
+    _nm_dhcp_client_notify(NM_DHCP_CLIENT(self),
+                           event == N_DHCP4_CLIENT_EVENT_GRANTED
+                               ? NM_DHCP_CLIENT_EVENT_TYPE_BOUND
+                               : NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED,
+                           l3cd);
 }
 
 static void
 dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
 {
-    NMDhcpNettoolsPrivate    *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    const NMDhcpClientConfig *client_config;
-    struct in_addr            server_id;
-    char                      addr_str[INET_ADDRSTRLEN];
-    int                       r;
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    struct in_addr         server_id;
+    struct in_addr         yiaddr;
+    char                   addr_str[INET_ADDRSTRLEN];
+    char                   addr_str2[INET_ADDRSTRLEN];
+    int                    r;
 
-    _LOGT("client event %d", event->event);
-    client_config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));
+    if (event->event == N_DHCP4_CLIENT_EVENT_LOG) {
+        _NMLOG(nm_log_level_from_syslog(event->log.level), "event: %s", event->log.message);
+        return;
+    }
+
+    if (!NM_IN_SET(event->event, N_DHCP4_CLIENT_EVENT_LOG)) {
+        /* In almost all events (even those that we don't expect below), we clear
+         * the currently granted lease. That is, because in GRANTED state we
+         * expect to follow up with accept/decline, and that only works while
+         * we are still in the same state. Transitioning away to another state
+         * (on most events) will invalidate that. */
+        nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
+        nm_clear_l3cd(&priv->granted.lease_l3cd);
+    }
 
     switch (event->event) {
     case N_DHCP4_CLIENT_EVENT_OFFER:
@@ -827,53 +858,51 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
             return;
         }
 
+        n_dhcp4_client_lease_get_yiaddr(event->offer.lease, &yiaddr);
+        if (yiaddr.s_addr == INADDR_ANY) {
+            _LOGD("selecting lease failed: no yiaddr address");
+            return;
+        }
+
         if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(self), &server_id)) {
             _LOGD("server-id %s is in the reject-list, ignoring",
                   nm_utils_inet_ntop(AF_INET, &server_id, addr_str));
             return;
         }
 
+        _LOGT("selecting offered lease from %s for %s",
+              _nm_utils_inet4_ntop(server_id.s_addr, addr_str),
+              _nm_utils_inet4_ntop(yiaddr.s_addr, addr_str2));
+
         r = n_dhcp4_client_lease_select(event->offer.lease);
         if (r) {
             _LOGW("selecting lease failed: %d", r);
             return;
         }
-        break;
+
+        return;
     case N_DHCP4_CLIENT_EVENT_RETRACTED:
     case N_DHCP4_CLIENT_EVENT_EXPIRED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_EXPIRE, NULL);
-        break;
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE, NULL);
+        return;
     case N_DHCP4_CLIENT_EVENT_CANCELLED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
-        break;
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
+        return;
     case N_DHCP4_CLIENT_EVENT_GRANTED:
-        priv->lease = n_dhcp4_client_lease_ref(event->granted.lease);
-        bound4_handle(self, event->granted.lease, FALSE);
-        break;
+        bound4_handle(self, event->event, event->granted.lease);
+        return;
     case N_DHCP4_CLIENT_EVENT_EXTENDED:
-        bound4_handle(self, event->extended.lease, TRUE);
-        break;
+        bound4_handle(self, event->event, event->extended.lease);
+        return;
     case N_DHCP4_CLIENT_EVENT_DOWN:
         /* ignore down events, they are purely informational */
-        break;
-    case N_DHCP4_CLIENT_EVENT_LOG:
-    {
-        NMLogLevel nm_level;
-
-        nm_level = nm_log_level_from_syslog(event->log.level);
-        if (nm_logging_enabled(nm_level, LOGD_DHCP4)) {
-            nm_log(nm_level,
-                   LOGD_DHCP4,
-                   NULL,
-                   NULL,
-                   "dhcp4 (%s): %s",
-                   client_config->iface,
-                   event->log.message);
-        }
-    } break;
+        _LOGT("event: down (ignore)");
+        return;
     default:
-        _LOGW("unhandled DHCP event %d", event->event);
-        break;
+        _LOGE("unhandled DHCP event %d", event->event);
+        nm_assert(event->event != N_DHCP4_CLIENT_EVENT_LOG);
+        nm_assert_not_reached();
+        return;
     }
 }
 
@@ -896,7 +925,7 @@ dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
          */
         _LOGE("error %d dispatching events", r);
         nm_clear_g_source_inst(&priv->event_source);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return G_SOURCE_REMOVE;
     }
 
@@ -1008,46 +1037,65 @@ nettools_create(NMDhcpNettools *self, GError **error)
 }
 
 static gboolean
-_accept(NMDhcpClient *client, GError **error)
+_accept(NMDhcpClient *client, const NML3ConfigData *l3cd, GError **error)
 {
     NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     int                    r;
 
-    g_return_val_if_fail(priv->lease, FALSE);
-
     _LOGT("accept");
 
-    r = n_dhcp4_client_lease_accept(priv->lease);
+    g_return_val_if_fail(l3cd, FALSE);
+
+    if (priv->granted.lease_l3cd != l3cd)
+        return TRUE;
+
+    nm_assert(priv->granted.lease);
+
+    r = n_dhcp4_client_lease_accept(priv->granted.lease);
+    if (!r)
+        lease_save(self, priv->granted.lease, priv->lease_file);
+
+    nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
+    nm_clear_l3cd(&priv->granted.lease_l3cd);
+
     if (r) {
         set_error_nettools(error, r, "failed to accept lease");
         return FALSE;
     }
 
-    priv->lease = n_dhcp4_client_lease_unref(priv->lease);
-
     return TRUE;
 }
 
 static gboolean
-decline(NMDhcpClient *client, const char *error_message, GError **error)
+decline(NMDhcpClient *client, const NML3ConfigData *l3cd, const char *error_message, GError **error)
 {
     NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     int                    r;
+    nm_auto(n_dhcp4_client_lease_unrefp) NDhcp4ClientLease *lease = NULL;
 
-    g_return_val_if_fail(priv->lease, FALSE);
+    _LOGT("decline (%s)", error_message);
 
-    _LOGT("dhcp4-client: decline (%s)", error_message);
+    g_return_val_if_fail(l3cd, FALSE);
+
+    if (priv->granted.lease_l3cd != l3cd) {
+        nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "calling decline in unexpected state");
+        return FALSE;
+    }
+
+    nm_assert(priv->granted.lease);
+
+    lease = g_steal_pointer(&priv->granted.lease);
+    nm_clear_l3cd(&priv->granted.lease_l3cd);
+
+    r = n_dhcp4_client_lease_decline(lease, error_message);
 
-    r = n_dhcp4_client_lease_decline(priv->lease, error_message);
     if (r) {
         set_error_nettools(error, r, "failed to decline lease");
         return FALSE;
     }
 
-    priv->lease = n_dhcp4_client_lease_unref(priv->lease);
-
     return TRUE;
 }
 
@@ -1256,7 +1304,8 @@ dispose(GObject *object)
 
     nm_clear_g_free(&priv->lease_file);
     nm_clear_g_source_inst(&priv->event_source);
-    nm_clear_pointer(&priv->lease, n_dhcp4_client_lease_unref);
+    nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
+    nm_clear_l3cd(&priv->granted.lease_l3cd);
     nm_clear_pointer(&priv->probe, n_dhcp4_client_probe_free);
     nm_clear_pointer(&priv->client, n_dhcp4_client_unref);
 
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index 4a718de9..f2dd1823 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -459,7 +459,7 @@ bound4_handle(NMDhcpSystemd *self, gboolean extended)
 
     if (sd_dhcp_client_get_lease(priv->client4, &lease) < 0 || !lease) {
         _LOGW("no lease!");
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
     }
 
@@ -473,15 +473,16 @@ bound4_handle(NMDhcpSystemd *self, gboolean extended)
     if (!l3cd) {
         _LOGW("%s", error->message);
         g_clear_error(&error);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
     }
 
     dhcp_lease_save(lease, priv->lease_file);
 
-    nm_dhcp_client_set_state(NM_DHCP_CLIENT(self),
-                             extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND,
-                             l3cd);
+    _nm_dhcp_client_notify(NM_DHCP_CLIENT(self),
+                           extended ? NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED
+                                    : NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
+                           l3cd);
 }
 
 static int
@@ -500,10 +501,10 @@ dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
 
     switch (event) {
     case SD_DHCP_CLIENT_EVENT_EXPIRED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_EXPIRE, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_EXPIRE, NULL);
         break;
     case SD_DHCP_CLIENT_EVENT_STOP:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         break;
     case SD_DHCP_CLIENT_EVENT_RENEW:
     case SD_DHCP_CLIENT_EVENT_IP_CHANGE:
@@ -876,7 +877,7 @@ bound6_handle(NMDhcpSystemd *self)
 
     if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) {
         _LOGW(" no lease!");
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
     }
 
@@ -892,11 +893,11 @@ bound6_handle(NMDhcpSystemd *self)
 
     if (!l3cd) {
         _LOGW("%s", error->message);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
     }
 
-    nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_BOUND, l3cd);
+    _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_BOUND, l3cd);
 
     sd_dhcp6_lease_reset_pd_prefix_iter(lease);
     while (!sd_dhcp6_lease_get_pd(lease,
@@ -921,11 +922,11 @@ dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data)
 
     switch (event) {
     case SD_DHCP6_CLIENT_EVENT_RETRANS_MAX:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_TIMEOUT, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT, NULL);
         break;
     case SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE:
     case SD_DHCP6_CLIENT_EVENT_STOP:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL);
+        _nm_dhcp_client_notify(NM_DHCP_CLIENT(user_data), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         break;
     case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE:
     case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST:
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 081e2841..88fe83f3 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -855,8 +855,7 @@ nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
     const char                             *start;
     const char                             *iaid;
 
-    nm_assert(out_l3cd_merged);
-    nm_assert(!*out_l3cd_merged);
+    nm_assert(out_l3cd_merged && !*out_l3cd_merged);
 
     if (!l3cd_old)
         return FALSE;
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index c8b789b3..60c286ee 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -2815,7 +2815,10 @@ _host_id_read(guint8 **out_host_id, gsize *out_host_id_len)
         int    base64_save  = 0;
         gsize  len;
 
-        success = nm_utils_random_bytes(rnd_buf, sizeof(rnd_buf));
+        if (nm_random_get_crypto_bytes(rnd_buf, sizeof(rnd_buf)) < 0)
+            nm_random_get_bytes_full(rnd_buf, sizeof(rnd_buf), &success);
+        else
+            success = TRUE;
 
         /* Our key is really binary data. But since we anyway generate a random seed
          * (with 32 random bytes), don't write it in binary, but instead create
@@ -3313,7 +3316,7 @@ nm_utils_stable_id_random(void)
 {
     char buf[15];
 
-    nm_utils_random_bytes(buf, sizeof(buf));
+    nm_random_get_bytes(buf, sizeof(buf));
     return g_base64_encode((guchar *) buf, sizeof(buf));
 }
 
@@ -3684,7 +3687,7 @@ nm_utils_hw_addr_gen_random_eth(const char *current_mac_address,
 {
     struct ether_addr bin_addr;
 
-    nm_utils_random_bytes(&bin_addr, ETH_ALEN);
+    nm_random_get_bytes(&bin_addr, ETH_ALEN);
     _hw_addr_eth_complete(&bin_addr, current_mac_address, generate_mac_address_mask);
     return nm_utils_hwaddr_ntoa(&bin_addr, ETH_ALEN);
 }
diff --git a/src/core/nm-l3-config-data.c b/src/core/nm-l3-config-data.c
index 5ab2f6b8..2a722756 100644
--- a/src/core/nm-l3-config-data.c
+++ b/src/core/nm-l3-config-data.c
@@ -2275,6 +2275,8 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
         }
 
         if (NM_FLAGS_HAS(flags, NM_L3_CONFIG_CMP_FLAGS_DNS)) {
+            const NML3ConfigDatFlags FLAG = NM_L3_CONFIG_DAT_FLAGS_HAS_DNS_PRIORITY(IS_IPv4);
+
             NM_CMP_RETURN(_garray_inaddr_cmp(a->nameservers_x[IS_IPv4],
                                              b->nameservers_x[IS_IPv4],
                                              addr_family));
@@ -2283,7 +2285,8 @@ nm_l3_config_data_cmp_full(const NML3ConfigData *a,
             NM_CMP_RETURN(
                 nm_strv_ptrarray_cmp(a->dns_options_x[IS_IPv4], b->dns_options_x[IS_IPv4]));
 
-            if (NM_FLAGS_ANY(a->flags, NM_L3_CONFIG_DAT_FLAGS_HAS_DNS_PRIORITY(IS_IPv4)))
+            NM_CMP_DIRECT(NM_FLAGS_ANY(a->flags, FLAG), NM_FLAGS_ANY(b->flags, FLAG));
+            if (NM_FLAGS_ANY(a->flags, FLAG))
                 NM_CMP_DIRECT(a->dns_priority_x[IS_IPv4], b->dns_priority_x[IS_IPv4]);
         }
 
diff --git a/src/core/nm-l3cfg.c b/src/core/nm-l3cfg.c
index 38b9d822..af6099e9 100644
--- a/src/core/nm-l3cfg.c
+++ b/src/core/nm-l3cfg.c
@@ -208,6 +208,12 @@ typedef struct {
     gboolean          force_commit_once : 1;
 } L3ConfigData;
 
+struct _NML3CfgBlockHandle {
+    NML3Cfg *self;
+    CList    lst;
+    gboolean is_ipv4;
+};
+
 /*****************************************************************************/
 
 NM_GOBJECT_PROPERTIES_DEFINE(NML3Cfg, PROP_NETNS, PROP_IFINDEX, );
@@ -251,6 +257,14 @@ typedef struct _NML3CfgPrivate {
 
     union {
         struct {
+            CList blocked_lst_head_6;
+            CList blocked_lst_head_4;
+        };
+        CList blocked_lst_head_x[2];
+    };
+
+    union {
+        struct {
             NMIPConfig *ipconfig_6;
             NMIPConfig *ipconfig_4;
         };
@@ -4240,20 +4254,29 @@ _l3_commit_one(NML3Cfg              *self,
                 g_array_append_val(ipv6_temp_addrs_keep, addr->address);
             }
         }
-        addresses_prune =
-            nm_platform_ip_address_get_prune_list(self->priv.platform,
+
+        if (c_list_is_empty(&self->priv.p->blocked_lst_head_x[IS_IPv4])) {
+            addresses_prune =
+                nm_platform_ip_address_get_prune_list(self->priv.platform,
+                                                      addr_family,
+                                                      self->priv.ifindex,
+                                                      nm_g_array_data(ipv6_temp_addrs_keep),
+                                                      nm_g_array_len(ipv6_temp_addrs_keep));
+
+            routes_prune = nm_platform_ip_route_get_prune_list(self->priv.platform,
+                                                               addr_family,
+                                                               self->priv.ifindex,
+                                                               route_table_sync);
+            _obj_state_zombie_lst_prune_all(self, addr_family);
+        }
+    } else {
+        if (c_list_is_empty(&self->priv.p->blocked_lst_head_x[IS_IPv4])) {
+            _obj_state_zombie_lst_get_prune_lists(self,
                                                   addr_family,
-                                                  self->priv.ifindex,
-                                                  nm_g_array_data(ipv6_temp_addrs_keep),
-                                                  nm_g_array_len(ipv6_temp_addrs_keep));
-
-        routes_prune = nm_platform_ip_route_get_prune_list(self->priv.platform,
-                                                           addr_family,
-                                                           self->priv.ifindex,
-                                                           route_table_sync);
-        _obj_state_zombie_lst_prune_all(self, addr_family);
-    } else
-        _obj_state_zombie_lst_get_prune_lists(self, addr_family, &addresses_prune, &routes_prune);
+                                                  &addresses_prune,
+                                                  &routes_prune);
+        }
+    }
 
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_ndisc_*(). */
     /* FIXME(l3cfg): need to honor and set nm_l3_config_data_get_mtu(). */
@@ -4374,6 +4397,48 @@ _l3_commit(NML3Cfg *self, NML3CfgCommitType commit_type, gboolean is_idle)
     _nm_l3cfg_emit_signal_notify_simple(self, NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT);
 }
 
+NML3CfgBlockHandle *
+nm_l3cfg_block_obj_pruning(NML3Cfg *self, int addr_family)
+{
+    const int           IS_IPv4 = NM_IS_IPv4(addr_family);
+    NML3CfgBlockHandle *handle;
+
+    if (!self)
+        return NULL;
+
+    nm_assert(NM_IS_L3CFG(self));
+
+    handle          = g_slice_new(NML3CfgBlockHandle);
+    handle->self    = g_object_ref(self);
+    handle->is_ipv4 = IS_IPv4;
+    c_list_link_tail(&self->priv.p->blocked_lst_head_x[IS_IPv4], &handle->lst);
+
+    _LOGT("obj-pruning for IPv%c: blocked (%zu)",
+          nm_utils_addr_family_to_char(addr_family),
+          c_list_length(&self->priv.p->blocked_lst_head_x[IS_IPv4]));
+
+    return handle;
+}
+
+void
+nm_l3cfg_unblock_obj_pruning(NML3CfgBlockHandle *handle)
+{
+    gs_unref_object NML3Cfg *self    = handle->self;
+    const int                IS_IPv4 = handle->is_ipv4;
+
+    nm_assert(NM_IS_L3CFG(self));
+    nm_assert(c_list_is_linked(&handle->lst));
+    nm_assert(c_list_contains(&self->priv.p->blocked_lst_head_x[IS_IPv4], &handle->lst));
+
+    c_list_unlink_stale(&handle->lst);
+
+    _LOGT("obj-pruning for IPv%c: unblocked (%zu)",
+          IS_IPv4 ? '4' : '6',
+          c_list_length(&self->priv.p->blocked_lst_head_x[IS_IPv4]));
+
+    nm_g_slice_free(handle);
+}
+
 /* See DOC(l3cfg:commit-type) */
 void
 nm_l3cfg_commit(NML3Cfg *self, NML3CfgCommitType commit_type)
@@ -4678,6 +4743,8 @@ nm_l3cfg_init(NML3Cfg *self)
     c_list_init(&self->priv.p->obj_state_lst_head);
     c_list_init(&self->priv.p->obj_state_temporary_not_available_lst_head);
     c_list_init(&self->priv.p->obj_state_zombie_lst_head);
+    c_list_init(&self->priv.p->blocked_lst_head_4);
+    c_list_init(&self->priv.p->blocked_lst_head_6);
 
     self->priv.p->obj_state_hash = g_hash_table_new_full(nmp_object_indirect_id_hash,
                                                          nmp_object_indirect_id_equal,
@@ -4726,6 +4793,8 @@ finalize(GObject *object)
     nm_assert(!self->priv.p->ipv4ll);
 
     nm_assert(c_list_is_empty(&self->priv.p->commit_type_lst_head));
+    nm_assert(c_list_is_empty(&self->priv.p->blocked_lst_head_4));
+    nm_assert(c_list_is_empty(&self->priv.p->blocked_lst_head_6));
 
     nm_assert(!self->priv.p->commit_on_idle_source);
 
diff --git a/src/core/nm-l3cfg.h b/src/core/nm-l3cfg.h
index f6ec39ce..215d21e9 100644
--- a/src/core/nm-l3cfg.h
+++ b/src/core/nm-l3cfg.h
@@ -455,4 +455,9 @@ struct _NMIPConfig *nm_l3cfg_ipconfig_acquire(NML3Cfg *self, int addr_family);
 
 /*****************************************************************************/
 
+typedef struct _NML3CfgBlockHandle NML3CfgBlockHandle;
+
+NML3CfgBlockHandle *nm_l3cfg_block_obj_pruning(NML3Cfg *self, int addr_family);
+void                nm_l3cfg_unblock_obj_pruning(NML3CfgBlockHandle *handle);
+
 #endif /* __NM_L3CFG_H__ */
diff --git a/src/core/nm-policy.c b/src/core/nm-policy.c
index 2d47772d..1a824fa4 100644
--- a/src/core/nm-policy.c
+++ b/src/core/nm-policy.c
@@ -2153,19 +2153,14 @@ device_l3cd_changed(NMDevice             *device,
 
     nm_dns_manager_begin_updates(priv->dns_manager, __func__);
 
-    /* We catch already all the IP events registering on the device state changes but
-     * the ones where the IP changes with a stable state (i.e., activated):
-     * ignore IP config changes but when the device is in activated state.
-     * Prevents unnecessary changes to DNS information.
-     * FIXME(l3cfg): check why ^^^ this is needed and implement it. Note that
-     * this function is not always called when the device becomes ACTIVATED.
-     * Previously, we would also update the DNS manager's IP config in
-     * device_state_change(ACTIVATED). There we would also special-case
-     * pseudo-VPNs like wireguard. I don't see the code where this is handled
-     * now.
+    /* FIXME(l3cfg): Note that this function is not always called when the
+     * device becomes ACTIVATED. Previously, we would also update the DNS
+     * manager's IP config in device_state_change(ACTIVATED). There we would
+     * also special-case pseudo-VPNs like wireguard. I don't see the code where
+     * this is handled now.
      */
     state = nm_device_get_state(device);
-    if (l3cd_new && state > NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
+    if (l3cd_new && state >= NM_DEVICE_STATE_IP_CONFIG && state < NM_DEVICE_STATE_DEACTIVATING) {
         nm_dns_manager_set_ip_config(priv->dns_manager,
                                      AF_UNSPEC,
                                      device,
diff --git a/src/core/platform/tests/test-platform-general.c b/src/core/platform/tests/test-platform-general.c
index 93e525dd..908ad4dd 100644
--- a/src/core/platform/tests/test-platform-general.c
+++ b/src/core/platform/tests/test-platform-general.c
@@ -101,146 +101,156 @@ static void
 test_platform_ip_address_pretty_sort_cmp(gconstpointer test_data)
 {
     static const char *const EXPECTED_BUFFER[3] = {
-        [0] = ("5b1aea34648cabfec7c3523f76cf1ce34ca17a9a32f3f0f218024e48836dd1cb504e03d53e1124c5"
-               "0065aeb2e6fbf952902383028e3b47f280f062ea1a7e0b7be218d067530e1b0487b8c3b99f2b8a1a"
-               "8907c42f0000003437c5156e072b2f2f0037c9cfe07c34ddb3980deb14ab7b5af84a034703000000"
-               "883b0f3fd6ed84d6c959e553b887edcd6101f5d200000043b809d259e499db7d00f1853bdcb0e4bc"
-               "0e2b00b667b7b16d8d1e69c803000000b973972c17a47631c169f11ff9119c40b403b6630000007a"
-               "034f43340d01683c0045097aea4a849f060ddf57b24a5be9636360d603000000ad7c499dd538d345"
-               "74c038404923e75d0e02e2fc0000004acc807cdce682f80f00315c45ef817264c89a736ed55ed637"
-               "b96c200203000000faf1809becd2506315a6da29b2e94d3ab503e0e900000083a36035fb6297dfa7"
-               "00686b5efd0d53bb6215de4bb6f6f3d031a79028030000008bb836c0a25ea71f5daaed4d99eb2ebe"
-               "be03432f000000bf4ccf30d3aaaf02a4005d7308b67f91bf9d82c856ba942455e8d07c8403000000"
-               "f2abb982b001ec16901f55f960c55c22f30299a80000002b4d4647f53b1921af0088e3759a08e7a5"
-               "6663861eea1bf42c12ea3b9503000000fb95e8332fdfff658483a2d039a7bf148e02481e00000060"
-               "e89f7abdb682380a00eae374835b4a49a2b980b6aba92da6409969aa03000000e00473755d31e5b2"
-               "de252167c1c91b3a36020c700000007740318db913a353ed006efc068829c0e66ad0143a0554efb4"
-               "d149f07403000000153ece68ade15cec25a59273e7519f34c40d8d70000000f3819aa46fbe143934"
-               "0033ae6dec0fb124f264af67eed7c9a840e55b8b03000000c0cfb2b4386bec092fa5757ecde9348b"
-               "da002ebd000000ab667224dae775e5cc0041aca2ff0f576767d3648102b61886ecc8fb1103000000"
-               "fcebbaeb0c56535923f14874042a8aff1d028e5ec3cc13cc36bbe3c9bb0ec36f00e007bb64a2827b"
-               "7cdd38d0314c178e5a06c40b03000000ab90135fa636af4464d210a256be75e0500244770000004b"
-               "2e69220d6c0fc09c25d6534c809829af4a9df58dbfef186d416f3a1e030000002c932e655203d82a"
-               "3c84c4eb274ed18687030281000000f2235376239daeaacb3cae864b437baaae91921681c2162b9b"
-               "69e66142030000004fda8a3e0b841cf76391dd68269b53eccb02a831000000b78c54dda9ac3bb1b1"
-               "d43e6505621b9a7f0422ae3fc8979ee0416f95e70300000057d6249b652ba98c7dc7f17f666969e4"
-               "5c02af7a000000ee0b06fa9e988f80f0de6dc8dfcf2a3ad3bbcc0fa3b314f695b550c5db03000000"
-               "f32f4af595d785d5c1b5aad487c192f08b12a09b00000043288cd9bf66ec305a225a0c71b2ce78bb"
-               "16104c8eaf18c565111d891d03000000cd897619f51f44e644d7cf1d06b2b115d800549e62c12fba"
-               "5b1cdec48d10bbb13b8313d8cd2a24d34fc812bd2f8a59d90fcc00ac030000005292cd32dc096cd5"
-               "d8a4c5cf3351ee1cfc03056c00000051bbce426cfa4b861cc78592be7b14e7ba9c15acb881ae55f0"
-               "b16c7376030000005219061ab4c5c79489b2cc6a883c14697205cb8b000000f5e6d66df46ea13910"
-               "7754dee62c36d2fc70ccc567df7a49b8e5fe7d360300000066a3ae3939762df33a2d55060c78d551"
-               "cb0010fd00000041b9aca07b6e4925dd27943a272c171ed15abbbe1cd911db7b86ed271803000000"
-               "a5edc511c1507a141e0f515638c7ba31f50209450000003357ae79989870ccec3def0ad92749e016"
-               "663fe6ee0228c1dabb20413203000000862573c2303dd1d65c7b2cffeca6d1adac11e11f0000000f"
-               "855ebf3b772eb2b1c896c9a7304f66450a5f6abd850b06e382d1595603000000348352d715cf9d41"
-               "1ea012e5307294b6e301dac4000000075efee38dd16f8ee4ccd2f50c30706cae3fdcc2f0ee3d5e26"
-               "b10123e6030000001dff045298cfee0636674cdefb57b9ae541de8f4000000381ba2c4396de60f03"
-               "2bc7f34de2959871c0d4c0d4eb720c4a585287dc03000000cfb18b2b2cb749e2e03e544d0eb4f73e"
-               "751d9fcf251b32fc79685b05ddd3aa9ba511d2e40edb4d758fb554158ae5c7c0beb42b3403000000"
-               "895d5f24037d233302ad3b82d639272e4a02eadfbd2146bf8cfdb205f90e54b58a6ee136a779f37c"
-               "30d2c5053c40ecaec38b6b8e03000000bea73223e59bf0193432e9fa7a899f2d8e0ae4b89bf5a5d0"
-               "6776e66a9d80ab132e1ac921eb76adbb229df32e561fa80a0fc4676703000000a23eb66e720da9e0"
-               "7ac998b5690807d52602369ee1af4ca5a6a95581af5fd7ceacdca10f47d7b351a36b178aabc78a4a"
-               "1a0dd8c003000000e2815a1a37a52bddd1c2f1018b587eed720358f0e9201f17bd99fcf72909ac9e"
-               "7a55299e9bb4fd53bc7417940fcffe3f81cafd6302000000d6732578acd14320aefd4503189f7630"
-               "2403501c0000002b9f3c39f24b0572b100745cb25851429b3bbfb50168dfd04eabba4a3202000000"
-               "8ffe423d01883918039249f398f9b37ea0104651000000643722d9b707c0d8a400b7c8307f06b4b2"
-               "9088f20d9ac676d5b62f22ca02000000891715df7fc6a902edae579e2e10c7f7a202a0340436242c"
-               "beb0248cee3fbc160032d4f28aa28c08f80dd50c6712dfb4e4bafc4e02000000fde69eec3af2e6d0"
-               "bd68ab722af14548b20672e504265f6c72923e22594f3f790008ed2e2ebb0771db46a54cadb245ea"
+        [0] = ("5b1aea34648cabfec7c3523f76cf1ce34ca17a9a32f3f0f218074e48000000cb504e03d53e1124c5"
+               "0065aeb2e6fbf952902383028e3b47f2111d891d17a47631c169f11ff9119c40b468b66331bca67a"
+               "034f43340d02683c7545097aea4a849f060ddf57004a5be9b10123e6e4f15ecd1dff045298cfee06"
+               "b7f0318a03000000061db3dd7fde25654a4059d565dbc8a91e014457000000db56189572f32f4af5"
+               "00d785d5c1b5aad487c192f08bd7a09bdd2144439c88b02a0300000004b0595daf37deb499996bfb"
+               "667f072dec035d9c000000f4409bcfb3c167090e00d90a8543961b2ccd47724a3c460ba85183f4c5"
+               "a7afe28303000000d05ff720b16c73761a3fcb675219061ab402c7940000006a883c146972decb8b"
+               "00dac8f5e6d66df46ea139107754dee62c36d2fc6aae660b03000000fc1fb23d24438387def0f4c6"
+               "544e4b275d037146000000d349adc17647176b7c00d856e3b883efebc09dd9d61966b7ae7412041d"
+               "e5fe7d360300000066a3ae3939762df33a2d55060c78d551cb0310fd00000043b9aca07b6e4925dd"
+               "00943a272c171ed15abbbe1cd911db7b56e8f3fe6f0e9e6903000000ec2e2924348352d715cf9d41"
+               "1ea012e5300294b6000000c528ab20075efee38d006f8ee4ccd2f50c30706cae3fdcc2f0ee3d5e26"
+               "dac12ebd03000000667224dae775e5ccd141aca2ff0f57676702648100000086b62f22ca33478e1f"
+               "001715df7fc6a902edae579e2e10c7f7a22ba0340436242c03000000ee3fbc164632d4f28aa28c08"
+               "f80dd50c6702dfb400000023a28fa772a19cdf6d00c173f6078a806d329c9b008bbfc9723107f3f5"
+               "bcfe97e1030000007ad742d3d7320a4f880cf47f5dd0cf69cb0d840f0000007749509cb6752b2cee"
+               "00b7e7736a0afc9879ea40e69710fc9f6e8e99cdabf85f0c0300000008d33a2ea67776d7f88d69c8"
+               "c7e3b2d3c900f05400000021abb42316c533d9c90088189c471a1ff2f0ce3ff66e782110125ae2ed"
+               "922b6cb8030000001d5c302df59dd32a677e5a3af4fe297f6002f061d45d864d09f4ecf17625f82d"
+               "00c748288ae21e371051852c972ea7954079884a6c0fc09c03000000809829af4a9df58dbfef186d"
+               "416f3a1ef102d10f000000675203d82a3c84c4eb274ed18687780281d47a32f2235376239daeaacb"
+               "3cae864b0300000091921681c2162b9bfa40546cdb544e44df037c270000009a051bb2ee8192ac0c"
+               "fa3abc1ea0ab3e7d75a2f42b50c6a363401323780422ae3f03000000c43bd794c697895e5bfe47c6"
+               "f53a54e01b021d89000000d3e02d2ec587fc330003781b0f294a2b73c4421398f4de67e9cee64b38"
+               "ae8cb60b0300000038d856a40c67c3d4afa9c8649da90bd2ff02f2560000000bd7d5f14870c6d18d"
+               "6242b356955e1a13d1111b067acbce3e3bcdf382bb0ec36f0300000064a2827b7cdd38d0314c178e"
+               "925b568ae512c4e9000000e5264fd3ebe9cf9f6df9615189f78ee815b2781ea55c9555876b6a3f13"
+               "36674cde0300000054cfe8f42bf0e9381ba2c4396de60f032b00f34de2959871c0d4c0d4eb720c4a"
+               "e3c9472e5f2da5d9c5cfd9f2343b21362c19a09269121aec030000000fcc00ac2daa755e5292cd32"
+               "dc096cd5d802c5cf3351ee1cfcc2056cc1ff8e51bbce426cfa4b861cc78592be7b14e7ba9c15acb8"
+               "ba8110fb03000000904f288262e6ea77a22fa5f863eaeefa70031205000000f39b098b43cd68be6e"
+               "3f81268193fd637e585287dc6ca972a2cfb18b2bf6989d2d030000006ff7add5df2c93e05459507b"
+               "5d8c6a0f460554d2000000a8c67b2e48b47003c81785d3ffdbd9a617857ae8126b028c0e3e295354"
+               "bc51e540030000006c3d048a92d3adc69a84eb47622400207700416a00000087fbd7e2f7dea0077a"
+               "e9c0c0d9c386e9c6e6a2cbfa10ee58bdc75183607c52a52d030000004e8e8bc17c01ac63eca0c1cf"
+               "bb204132fb0213e4000000c2303dd1d65c7b2cffeca6d1adaccae11f8045a80f855ebf3b772eb2b1"
+               "bae87d60030000005d31e5b2de252167c1c91b3a36ba0c700e118477000000b913a353edc66efc06"
+               "8829c0e66ad0143a0554efb46b382ab0404cdf027a899f2d030000009bf5a5d06776e66a9d80ab13"
+               "2e1ac921eb01adbb0000002f561fa80a40e55b8b1dd92e18c0cfb2b4386bec092fa5757ecde9348b"
+               "daae7d6303000000c0982b283aa9986700b2a2b3ed257b8b041df48f000000cac72105b327335a25"
+               "a364d1015ffac03089f45539e1b0178462156cd6c4e1966803000000b3121cd5ef51e696c816290d"
+               "baee0e77260a82e1530ff5397c9125f59577d71c4b258a005116d11354edff62ceaa458fc75a91c4"
+               "d03bc90503000000eb760e3f004a2efc8ffc96b89b1bc8f415034e77be46bae5a1b3cae76665a268"
+               "abfe8a41d149f074a378b881153ece68ade15cec7625ca4a03000000e682f80f94315c45ef817264"
+               "c89a736ed51dd637b077ddad3108be69f9b97d05c917ad6b10e693bb6e26f2ba90c8e909e85e20e5"
+               "2af145480300000004265f6c72923e22594f3f79e208ed2e2e020771db46a54cadb245eafde6b025"
+               "67f814de39dd8f88152a58454d9ca9d20f45dfa700154d12020000009edfa7a74c1c83c18e47935e"
+               "8354846e3403386a000000d94064f6b021281e230094ae20ac26c7d41fd050bf8d85055bcfe3756c"
+               "98c39e7c02000000efc57ff447036b9551adcb22fd50793f79103c7d0000006e759d0c90bc512e13"
+               "00cef5e4a27985bd8d1e69c83dc21056b973972c6855676c02000000017c9148da5892bb4951c3a7"
+               "ed55689d3305ce7d3fd48469845f0c233dbdd2a90035df1f1782dee88487fd0db5971b46b7a350f9"
                "8c3b371502000000710c030690f5f18ea125dbf7d7e93bd65c01a56dfdcfc1155f236c8b9c79a620"
-               "00660bbf024b03ff0a8e27c405e64244e36f90d402000000fd41fe47684b370b6ec6584d64496089"
-               "570568ead4d1ae91c819bb068196d59900de3246e43f5e7945aaf95e2ffa3a11641e447502000000"
-               "b9b68c08a5e4351ea349e1ccf662e058b819a45100000045fcb6a035339d504c9726d80d9c2d89df"
-               "765b4d9a130257dc9c64ed0402000000a660ac824b7fae389861419c50da49bf2b02258300000025"
-               "9f9f0251becc987907879cca68fec7bbbb5f8edf248b4995d184e82002000000a19cdf6dd1c173f6"
-               "078a806d329c9b008b00c972000000f5b2cd3dbddc74e26de958e48d2ab8b0313e7f8933e315130f"
-               "d1e1b34902000000bcd7be07d78e6222e45aaf61814f703b401f5e6b000000cbaaa37b861e6d46da"
-               "fe7d6ec4ac1ea051010911915ddb05f2c43bd794010000005bfe47c6f53a54e01b0c1d89414d94d0"
-               "e0032ec50000000103781b0f294a2b7300421398f4de67e9cee64b38b56e03e01539ce4101000000"
-               "18238487a417f3da01d99dae5f190096cc012cab0000005b2363d13edc5aa115005eb914d8fbe9a4"
-               "fdb3d117d76b0de5bd82e9ea010000003d1b91caae8cb60b49ba9be338d856a40c02c3d400000064"
-               "9da90bd2fff2f2560046870bd7d5f14870c6d18d6242b356b9ef1b2101000000f90adac616a31dc2"
-               "e46a234558817151b300c9c900000073e64b0bd761fdf274005dca8ce1bd1871ae17bb4515856092"
-               "b4d9e89b010000006855676c277cf1bd017c9148da5892bb4903c3a70000009f3391ce7d3fd48469"
-               "005f0c233dbdd2a97835df1f1782dee86c1de913010000003ee1d76fc1ea76e98c9dfc78997ab53e"
-               "27001963f5b4cb2454830c68e44ea74b002b83f9b3bee14d861a4c9eeecc89f65408c1b701000000"
-               "d9e1825fa638e1af284a024b7f9e85ff730050a2000000276e08cb887464b93400c3127c750fabdd"
-               "69121aec129cdc690d58fbcc01000000d0d44ff1e51c35157666c05348e6f507ff00e89800000012"
-               "b900b2061d0c334b009f2dd1cdf64b0f9a60e0e289f08db3fde6b0250100000039dd8f88152a5845"
-               "4d9ca9d20f45dfa774028604000000fbcd7db68b9ae586da00b4070c50320427c4dd3d031e33f22c"
-               "210aeb09010000007000b96d06992b6a58acd3995b9663d2df02f333ef467092818aa77d6732b678"
-               "00844ef5a943825fcd743f59bd14c89bf121014a01000000efc67bce716c856e3973dc42a1003be9"
-               "4f0ed8ee0000009b5d5bbe6c10085f3d6176f78a19bb8df1804c122fce5078c1955e1a1301000000"
-               "7acbce3e3bcdf3824f1b134847ff26ba4400774900000017ec852d59f3d17232edd86ad6c3103a68"
-               "843a9aef34983882d3d38785010000005e0dfe491d1ba96742c7b5e02b2271229c01b02a06d0dc55"
-               "04b0595daf37deb499996bfb667f072dec1e5d9cdc8a11f4409bcfb30100000099d90a8543961b2c"
-               "cd47724a3c460ba85103f4c500000063eafb1ac4c0982b283aa9986700b2a2b3ed257b8b0489f48f"
-               "053ec8ca0100000027335a25a364d1015ffac03089f45539e1021784000000d7b83579b8da27345a"
-               "72437f9b6245de39ec9e71ee4b95150756e8f3fe010000000ea8042602a1f8e6f5657f3f9e3eb807"
-               "cb067645000000b8df6f628a70456d79f25d5895fb57fa60d9279fb2b8fcbac65ad47b8a01000000"
-               "da40d88d40a6d75bc404156225b7eedefc1e44574b15e2ae496ad01bf007eacb0a28aec868282510"
-               "b60291ea6480e356594f24a100000000e143fd52599364e13468f80fd514573fb517671c00000069"
-               "32d1d5f5d0ce2cf6007a70ba5193a162bc92ec1b11d9172c925b568a0000000087bb24e5264fd3eb"
-               "e9cf9f6df9615189f701e815000000a75c9555876b6a3f13002b6cb8360feaac1d5c302df59dd32a"
-               "7a859db500000000362956cd46646a0e222160e5f769bb295703ed370000005b6a813387e99bb834"
-               "009da86c64fefab2548759d313a5b92d8e47935e0000000034f0386a253c21d94064f6b021281e23"
-               "5f00ae20000000d71fd050bf8d85055b00e3756ccdb3455c60ca7b11c66af76e857ae81200000000"
-               "3e29535402e9b690c628d048eacce745ea053cb1000000b632ef3be6070dafa200187470e9da5570"
-               "9427c226d324d9a08487fd0d00000000b7a350f9fc1519defa7db45325456669371e2a3b000000dc"
-               "c405cbee5016c25200d8901d7a0165fe20744edb6ba04f14a4c73cf500000000a4bab14874afdf54"
-               "e6aae816430607ca061fe09818e9bbec5918c59068baf76a008940f6fc3bbdc7f6090f756aae660b"
-               "6e4c699300000000d9c1e67743efb54e54270e46042e91186a034e38000000376feecb80ac245409"
-               "c0becc271d9c2f67179bff0644399ae7df6b3542000000004c5cf8107ba282f4f983821918f93e74"
-               "2d00f0550000006f2292362e5d68265d9f98c82d9b7a559be3acf4fc36fa6b51e3c9472e00000000"
-               "c5cfd9f2343b21362c19a0921dce2f839200fa45000000270b9977e166bee737fe73670c439a644c"
-               "323b59b4cd20eb7dabea74f700000000f6989d2d6a909e986ff7add5df2c93e05412507b0000000f"
-               "466554d2ae4d52a8c67b2e48b47003c81785d3ffdbd9a617438a72e1000000002495a609675344f7"
-               "e2e3a5ebaec3c85f0a0042f70abe95c50345132a61eda239d9d083c3bf085387046ef8a36f0e9e69"
-               "59747cf200000000ab2dadc5a39411fd4ff1116d478987316a1f3fc2000000cfc6ebe434a7ae8ff0"
-               "40483e310819e3b10db116431ec6f7696b382ab0000000009a6ce5d906837dbca6a5ee19d6f63fe9"
-               "cb0301f7246f13b2050424a2b3a45ef7a029c896b4132bd895072cfeffe9d6815997069500000000"
-               "0d3c723b91adb0da7c4aa7e7eb5a15bcde035fb98b841fd84cc43c510385b9a4c2aac1d67a909b29"
-               "7c703915312e9c3cae02dfa000000000dd603bd35e7fa0f02f2f3313d8469d09a91709c0b7f0318a"
-               "575a4f8e061db3dd7fde25654a4059d565dbc8a91e3b4457b077ddad3108be69f9b97d05c917ad6b"
-               "10e693bb6e26f2ba900fe909a9fe20e5c7a4c656482a9b0d00625009a40aeb62a42b6a62548e3c38"
-               "cd3c72f203000000ca82ac5180101be4f85cef468ea086ea9a01fdc3a9fe1ec787bc45db7c52a52d"
-               "00bd39a44e8e8bc17c01ac63eca0c1cf5ff7f03a03000000c9a89192c1c8be55281a59d1fd338f35"
-               "7e00f8cea9fec34573654ea6624f138ef9531cd9367a02e4d241989477a363d5fa40546c03000000"
-               "df2d7c2790d3119a051bb2ee8192ac0cfa00bc1ea9fe3e7d75a2f42b50c6a36340132378b95c5931"
-               "3bacba64dbe996203b02239e0300000024438387def0f4c6544e4b275d9b7146a70010d2a9fec176"
-               "47176b7c07d856e3b883efebc09dd9d61966b7ae7412041d57393c6f03000000182c0287822a272b"
-               "ec4501a1e27acfee7a0188ffa9fe6cae426de59560fad65d67c624f285d7174177a47579dda0b6ea"
-               "a9a84c820300000070b1646d8026e9f1704f1b16286ba2dabc01f082a9feed33ef60a8b540b26f66"
-               "761d1f13badfad0fe8fa8f3c1aad2a826e6904f50200000047150b9b14010469823acb72bb89182f"
-               "93002196a9feb9153b36bc60be5b534e006527f67485ab35aca0c7ee419733853cf09e8b02000000"
-               "e79c10acfce165e332a62384ec04e5bab40085ada9fe0070a36dd51323b2c54200154d12f86c260a"
-               "9edfa7a74c1c83c1050f63f802000000443cacf59c6379a44b7892f487afa98cb102c19ca9fe942f"
-               "460bcea75481f25e007d0de9a7afe283bd2f22ead05ff72006c83bc0020000004bdade862c224f6f"
-               "36506ebd455e679cef009bb8a9fecec3f8c8fa6867a982be8a934f852cc3d4d82bc0ec7303f99f8f"
-               "def85b7502000000a0bef8675b29a197b7b3cceaf5f1bb12c503256aa9fe6e5d58099ffc4a503a71"
-               "2350acbd48411f0dc15d2f0f49dad345d966279502000000e06302aba042aaa218dc091e9aa1477f"
-               "6f059830a9fec95829a8838314dff34d24c332219a1b163a732d803e0e2f4f916d06412601000000"
-               "98c39e7cc282208fefc57ff447036b955101cb22a9fe793f797a3c7dadd1c86e009d0c90bc512e13"
-               "7dcef5e4a27985bd5cfd5ce601000000152f2b70eaef7443e0f79ab6902dde533601ec71a9fe9f25"
-               "4ac95883195580410062ed564153e17478f8c3344d89c0bbfaa100fd01000000be184524a6bdc878"
-               "9cf851782d895bcc8d038489a9fe8c1287e6f7bb020ffdb00012098610e52bb2a16a4008aefd545b"
-               "0d80684e01000000ba8110fb9733cc24904f288262e6ea77a203a5f8a9feeefa701d120523bd98f2"
-               "00098b43cd68be6e3f81268193fd637e9037d7a701000000c47cf0f551e96770a754ac19ef820fe0"
-               "2900f2d3a9fe049150b8d10ab700cc3a7cf51be0403b654ba2f56808092069af5f5b481b01000000"
-               "68cb3bc873b04d937a6ed8f7bc51e5406617d098a9fe048a92d3adc69a84eb47622400207799416a"
-               "f1f0a086fbd7e2f7dea0077a00000000c386e9c6e6a2cbfa10ee58bdc75183609900d627a9feb1cb"
-               "e491cbbbf9443fd6007eb3c5bf64b671d6f18dbf463f9b83f512dc1c00000000fbab244735d67c61"
-               "283031667b2d74a10302b1b1a9fe2aa590a2312e17f1a35900459582f4ef43c780908872746e39ef"
-               "a9a89f8700000000fcdf6d9be94030b34774d1d7dddedd989902f627a9fe965a87041331b2834bcf"
-               "00b4e3ce848518c4e3f6cbf25e5e1b992231bb0200000000173c333cd03bc905b7b899afeb760e3f"
-               "00022efca9fe96b89b1bc8f415bd4e77be46bae5a1b3cae76665a268abfe8a41a84e27c100000000"
-               "cb29efdf672d2fa57fc85ebbe276c5661a03192da9fee7af5eb888e9eb37bb046686943b101e1f55"
-               "3215abf8fbdf17c3677e5a3a00000000608df061d45d864d09f4ecf17625f82da1034828a9fe1e37"
-               "1051852c972ea7954079884af257b044fd13a6826a4c619f3d136cac000000009402a4c216772167"
-               "3f2b02b3256ead1f2f039bc1a9fefca162fb81e733cff620ca7feefe1933631e8e69f6d9d6962d2c"),
+               "00660bbf024b03ff0a8e27c405e6424457393c6f74993ade02000000e62112898dab2dade2ab2fc9"
+               "c56a7c86be0296228ec3f2f439d949b22b9c161c00b745cebdd06352cb2c354434819f4b248eb2b8"
+               "6297dfa702000000fd0d53bb6215de4bb6f6f3d031a790287e06e9c18bb836c0a25ea71f5daaed4d"
+               "00eb2ebebe3c432fec7d3abc4ccf30d3aaaf02a46e91b91502000000be5b534e0a6527f67485ab35"
+               "aca0c7ee4102338500000063e66e3a23d6732578acd14320aefd4503189f7630248e501c9052c22b"
+               "e3ff1a5f02000000b4425486c619147eb0216050ed7afd741000e83600000011f9c797c0c0ec468e"
+               "9de5a9caf493472201e48d163106cc397446a33fa349e1cc02000000b879a45176740744fcb6a035"
+               "339d504c9719d80d000000df765b4d9a130257dc5ff7f03a9518862bc9a89192c1c8be55281a59d1"
+               "bcd7be0702000000e45aaf61814f703b40125e6baf4648cbaa1f7b86000000dafe7d6ec4ac1ea051"
+               "010911915ddb05f2a84e27c18a06e2c9cb29efdfb56e03e0010000002f70249eefc67bce716c856e"
+               "3973dc42a1033be9000000efb1f14d985d5bbe6c00085f3d6176f78a19bb8df1804c122fce5078c1"
+               "57d6249b010000007dc7f17f666969e45c3baf7a119a65ee0b01fa9e000000f3de6dc8dfcf2a3ad3"
+               "00cc0fa3b314f695bd82e9ea8891ed0a3d1b91ca4f1b134801000000449a774929e86716ec852d59"
+               "f3d17232ed026ad600000068843a9aef34983882004f24a17dba1ee7e143fd52599364e13468f80f"
+               "fc1519de010000002545666937c22a3b6e9686dcc405cbee5000c2520000001f7a0165fe20744edb"
+               "00a04f146e4c699341b95f41d9c1e67743efb54e54270e46010000006a894e387625da376feecb80"
+               "ac245409c003cc2700000067179bff0644399ae70064ed0432d599eaa660ac824b7fae389861419c"
+               "9f3c2c8801000000f51f44e644d7cf1d06b2b115d882549e62002fba5b1cdec48d10bbb13b8313d8"
+               "002a24d34fc812bd2f8a59d96d064126666befbb81ae55f0010000000aa20624fbab244735d67c61"
+               "283031667b0274a103e0b1b153222aa590a2312e00f1a35900459582f4ef43c780908872746e39ef"
+               "536c75bb01000000720da9e07ac998b5690807d52617369ee1004ca500000081af5fd7ceacdca10f"
+               "00d7b351a36b178aabc78a4a0d80684ee5dbf45bc7a4c65601000000cd625009a40aeb62a42b6a62"
+               "548e3c38b9002002000000fbfaf1809becd2506300a6da29b2e94d3ab51fe0e9af98b180a36035fb"
+               "f05d7308010000009d82c856ba942455050f63f8282f34644402acf5000000a54b7892f487afa98c"
+               "0021c19c69d9942f460bcea75481f25e307d0de959747cf201000000ab2dadc5a39411fd4ff1116d"
+               "478987316a0e3fc2000000cfc6ebe434a7ae8ff040483e310819e3b10db116431ec6f7691539ce41"
+               "d514573f01000000f6932f6932d1d5f5d0ce2cf6bf7a70ba5100a1620000001b11d9172cf84a0347"
+               "8b82cdd8883b0f3fd6ed84d6c959e553b887edcdf90e54b501000000a779f37c30d2c5053c40ecae"
+               "210aeb0934012a247000b96d06992b6a58acd3995b9663d2df48f333ef467092818aa77d6732b678"
+               "cf844ef501000000cd743f59bd14c89b5cfd5ce6e82e785415032b7000000043e0f79ab6902dde53"
+               "36b3ec7127e19f254ac9588319558041fc62ed564153e174010000004d89c0bb5408c1b7d17fb084"
+               "d9e1825fa602e1af0000004b7f9e85ff73e050a2cd89fd276e08cb887464b9344ec3127c750fabdd"
+               "9985d62701000000e491cbbbf9443fd6a77eb3c5bf64b671d6068dbf0000008306c83bc026977911"
+               "4bdade862c224f6f36506ebd455e679cef369bb8fc2b445701000000496ad01bf007eacb0a28aec8"
+               "68282510b61e91ea6480e356def85b75827b26e8a0bef8675b29a197b7b3cceaf5f1bb12c535256a"
+               "61c2f5d200000000b809d259e499db7dc8f1853bdcb0e4bc0e1700b60000006dae02dfa0c26e7c3a"
+               "00603bd35e7fa0f02f2f3313d8469d09a92409c0f257b044000000006a4c619f9037d7a754655354"
+               "c47cf0f5510167700000001bef820fe02931f2d30075049150b8d10ab700cc3a7cf51be0403b654b"
+               "10613fc90000000016a31dc2e46a234558817151b38fc9c909038d71000000d761fdf274fc5dca8c"
+               "00bd1871ae17bb4515856092b4d9e89b676761b2eecc89f600000000d635edea8ffe423d01883918"
+               "039249f39800b37e00000053b2a16e643722d9b700c0d8a41fb7c8307f06b4b29088f20d9ac676d5"
+               "4d4647f5000000000f88e3759a08e7a56663861eea1bf42ccd0572f200000057ca82ac5180101be4"
+               "005cef468ea086ea9aaafdc388811ec787bc45db50da49bf00000000d94beb259f9f0251becc9879"
+               "07879cca681ec7bb000000df248b49953d136cac00054b019402a4c2167721673f2b02b3256ead1f"
+               "02e9b69000000000eacce745ea213cb1a84035b632ef3be6071fafa202187470e9da55709427c226"
+               "0024d9a086ed27184b443181a5edc511c1507a142fea9bc10000000062fb81e733cff620ca7feefe"
+               "1933631e8e03f6d90000002f6e6904f57239c09c47150b9b14010469823acb72bb89182f93112196"
+               "cdb3455c00000000c66af76e80f062ea1a7e0b7be218d06753001b04000000bb9f2b8a1a8982c42f"
+               "53700a3437c5156e072b2f2fb337c9cfe07c34ddb83579b80000000072437f9b6245de39ec9e71ee"
+               "4b951507be002b340000000f895d5f24037d233302ad3b82d639272e4a46eadfbd2146bf8cfdb205"
+               "ebfdcec30000000067a982be8a934f852cc3d4d82bc0ec7303129f8f00000087965a316af2abb982"
+               "b001ec16901f55f960c55c22f32099a8020bea2a25a5927300000000c4458d70911714f1819aa46f"
+               "be143934c900ae6dec0fb124f264af67eed7c9a8c38b6b8ee2614344bea73223e59bf0193432e9fa"
+               "b4d54acb00000000a417f3da01d99dae5f190096cc582cabd51fd15a0000003edc5aa115165eb914"
+               "d8fbe9a4fdb3d117d76b0de5416f95e7bb75d72f4cc43c5100000000c2aac1d67a909b297c703915"
+               "312e9c3c5a03c40b20ea99d5ab90135fa636af4464d210a256be75e0509c4477d19ac64b2e69220d"
+               "288cd9bf00000000225a0c71b2ce78bb16104c8eaf18c565ec17fb112087b97cfcebbaeb0c565359"
+               "23f14874042a8aff1d1f8e5ec3cc13cc36bbe3c9b2cd3dbd00000000e958e48d2ab8b0313e7f8933"
+               "e315130fa9034c8288f83f6e70b1646d8026e9f1704f1b16286ba2dabc7ef0820c91ed33ef60a8b5"
+               "70ccc567df7a49b8a4c73cf5ffa4ffc8a4bab14874afdf54e60fe816a9fe07ca0675e09818e9bbec"
+               "0018c59068baf76ab08940f6fc3bbdc7f6090f7512ea3b9503000000fb95e8332fdfff658483a2d0"
+               "39a7bf148e01481ea9fec460e89f7abdb682380a00eae374835b4a49a2b980b6aba92da6409969aa"
+               "fd338f35030000006e8cc34573654ea6624f138ef9531cd9360002e4a9fe989477a363d55ad47b8a"
+               "11090df8da40d88d40a6d75bc404156225b7eedeb95c593103000000dbe9962069e66142e2275334"
+               "4fda8a3e0b001cf7a9fedd68269b53eccb44a831714acfb58c54dda9ac3bb1b1d43e6505621b9a7f"
+               "1e0f515603000000f57609453df7803357ae79989870ccec3d000ad9a9fee016663fe6ee0228c1da"
+               "5f5b481b9f05a16768cb3bc873b04d937a6ed8f7d7ac5f9903000000822a272bec4501a1e27acfee"
+               "7a8588ffd5016caea9fee59560fad65d67c624f285d7174177a47579dda0b6ead1e1b34901eaedd5"
+               "dc335c8b030000000a3c8e7c91bf821c4b09cee3c37ff4283a011480a9fec42cac4ef3ad378ffec7"
+               "085a62475588456e4ead7cbda620a3abae816e345d68265d020000009b7a559be3acf4fc36fa6b51"
+               "3cf09e8b99009462a9fe10acfce165e332a623840004e5bab49185ad9b7d0070a36dd51323b2c542"
+               "dedea2030200000002a1f8e6f5657f3f9e3eb807cbad76451002edb8a9fe628a70456d79f25d5895"
+               "0057fa60d9279fb2b8fcbac6abea74f7a6df7979b3980deb020000006c1de913df1424ce3ee1d76f"
+               "c1ea76e98c00fc78a9feb53e27c21963f5b4cb2454830c68e44ea74b4f2b83f9b3bee14d861a4c9e"
+               "bc74179402000000438a72e12ae0436a2495a609675344f7e200a5eba9fec85f0a1742f70abe95c5"
+               "0345132a61eda239d9d083c3bf085387046ef8a3b0916e5d020000004a503a712350acbd48411f0d"
+               "c15d2f0f4903d345a9fe06954d2471f00d3c723b91adb0da7c4aa7e7eb5a15bcde015fb98b841fd8"
+               "9a6ce5d902000000a6a5ee19d6f63fe9cbdd01f7246f13b2050524a2a9fe5ef7a029c896b4132bd8"
+               "95072cfeffe9d6812231bb02f922259b173c333c1dce2f83010000003a3efc240b9977e166bee737"
+               "fe73670c4301644ca9fe59b4cd20eb7dd3d38785008ffad45e0dfe491d1ba96742c7b5e02b227122"
+               "a9a89f8701000000fcdf6d9be94030b34774d1d7dddedd989901f627a9fe965a87041331b2834bcf"
+               "00b4e3ce848518c4e3f6cbf25e5e1b990fc46767e36f90d401000000fd41fe47684b370b6ec6584d"
+               "64496089570368eaa9feae91c819bb068196d59900de3246e43f5e7945aaf95e2ffa3a11b9ef1b21"
+               "74e7860401000000cd7db68b9ae586dacdb4070c50320427c4033d03a9fef22c636360d696be085a"
+               "007c499dd538d34574c038404923e75d0e09e2fcaefd545b010000007ced4f8b362956cd46646a0e"
+               "222160e5f700bb29a9feed37a2123c5b6a813387e99bb834a19da86c64fefab2548759d313a5b92d"
+               "c896c9a7010000000a5f6abd850b06e3d9662795ce2cc1cee01702aba9feaaa218dc091e9aa1477f"
+               "6fdc9830d7fbc95829a8838314dff34d24c332219a1b163a000000000e2f4f911a0dd8c052ed6b13"
+               "e2815a1a37002bdda9fef1018b587eed72bb58f000201f17bd99fcf72909ac9e7a55299e9bb4fd53"
+               "7666c05300000000ffa8e89871fb9510b900b2061d0c334b81022dd1a9fe4b0f9a60e0e289f08db3"
+               "00bafc4e095a3230fde69eec3af2e6d0bd68ab722cb749e2000000000eb4f73e75039fcf251b32fc"
+               "79685b05dd02aa9ba9fed2e40edb4d758fb554158ae5c7c00d58fbcc58fc4c2fd0d44ff1e51c3515"
+               "40b26f6600000000badfad0fe8fa8f3c1aad2a82530b7447d902f401a9fe2f4c329f997184c9d257"
+               "8e4cf9c48147507e1cd59ea1ae6da48b1eba6d16672d2fa500000000e276c5661a64192db44ce7af"
+               "5eb888e9eb03bb04a9fe943b101e1f553215abf8fbdf17c3641e447514ca565fb9b68c08a5e4351e"
+               "a2f5680800000000faa100fdfdd68d34be184524a6bdc8789c035178a9fe5bcc8da98489c55b8c12"
+               "87e6f7bb020ffdb0e612098610e52bb2a16a40089f3c39f200000000de745cb25851429b3bbfb501"
+               "68dfd04edf033542a9fe5f574c5cf8107ba282f4f983821918f93e742d08f0550b55726f2292362e"),
         [1] = ("54270e46020000006a894e387625da376feecb80ac245409c058cc271d9c2f67179bff0644399ae7"
                "9c64ed0432d599eaa660ac824b7fae389861419c7a899f2d010000009bf5a5d06776e66a9d80ab13"
                "2e1ac921eb04adbb229df32e561fa80a40e55b8b1dd92e18c0cfb2b4386bec092fa5757ecde9348b"
diff --git a/src/core/platform/tests/test-route.c b/src/core/platform/tests/test-route.c
index 2d39cb37..85838267 100644
--- a/src/core/platform/tests/test-route.c
+++ b/src/core/platform/tests/test-route.c
@@ -1390,12 +1390,17 @@ again_table:
         rr->protocol = _rr_rand_choose_u8(nmtst_get_rand_uint32());
 
 #define IPTOS_TOS_MASK 0x1E
+#define INET_DSCP_MASK 0xFC
 
 again_tos:
     rr->tos = _rr_rand_choose_u8(nmtst_get_rand_uint32());
+
     if (rr->addr_family == AF_INET && rr->tos & ~IPTOS_TOS_MASK)
         goto again_tos;
 
+    if (rr->tos & ~INET_DSCP_MASK)
+        goto again_tos;
+
     if (_rule_check_kernel_support(platform, FRA_IP_PROTO))
         rr->ip_proto = _rr_rand_choose_u8(nmtst_get_rand_uint32());
 
diff --git a/src/core/ppp/nm-ppp-manager.c b/src/core/ppp/nm-ppp-manager.c
index 896c233d..003a9806 100644
--- a/src/core/ppp/nm-ppp-manager.c
+++ b/src/core/ppp/nm-ppp-manager.c
@@ -545,7 +545,6 @@ impl_ppp_manager_set_ip4_config(NMDBusObject                      *obj,
                                  NM_IP_CONFIG_SOURCE_PPP);
 
     nm_l3_config_data_set_mtu(l3cd, mtu);
-    nm_l3_config_data_set_dns_priority(l3cd, AF_INET, 0);
 
     address = (NMPlatformIP4Address){
         .plen = 32,
@@ -642,6 +641,7 @@ impl_ppp_manager_set_ip6_config(NMDBusObject                      *obj,
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     NMPlatformIP6Address                    address;
     struct in6_addr                         a;
+    guint32                                 mtu;
     NMUtilsIPv6IfaceId                      iid         = NM_UTILS_IPV6_IFACE_ID_INIT;
     gboolean                                has_peer    = FALSE;
     gs_unref_variant GVariant              *config_dict = NULL;
@@ -652,13 +652,15 @@ impl_ppp_manager_set_ip6_config(NMDBusObject                      *obj,
 
     nm_clear_g_source(&priv->ppp_timeout_handler);
 
-    if (!set_ip_config_common(self, config_dict, NULL))
+    if (!set_ip_config_common(self, config_dict, &mtu))
         goto out;
 
     l3cd = nm_l3_config_data_new(nm_platform_get_multi_idx(NM_PLATFORM_GET),
                                  priv->ifindex,
                                  NM_IP_CONFIG_SOURCE_PPP);
 
+    nm_l3_config_data_set_mtu(l3cd, mtu);
+
     address = (NMPlatformIP6Address){
         .plen        = 64,
         .addr_source = NM_IP_CONFIG_SOURCE_PPP,
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index a4e5cc4d..22c422a2 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -1088,6 +1088,14 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                     error))
                 return FALSE;
         }
+
+        /* In case the connection is saved as OWE / Enhanced Open, prevent
+         * unencrypted downgrade
+         */
+        if (nm_streq(key_mgmt, "owe")) {
+            if (!nm_supplicant_config_add_option(self, "owe_only", "1", -1, NULL, error))
+                return FALSE;
+        }
     }
 
     /* WEP keys if required */
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index e502ae85..e7ac0e58 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -21,6 +21,7 @@
 #include "nm-supplicant-manager.h"
 
 #define DBUS_TIMEOUT_MSEC 20000
+#define PMK_LIFETIME_SEC  (3600 * 24 * 7)
 
 /*****************************************************************************/
 
@@ -1249,6 +1250,7 @@ parse_capabilities(NMSupplicantInterface *self, GVariant *capabilities)
                 break;
             }
         }
+        g_free(array);
     }
 
     priv->iface_capabilities = NM_SUPPL_CAP_MASK_SET(priv->iface_capabilities,
@@ -2451,6 +2453,32 @@ assoc_set_ap_scan_cb(GVariant *ret, GError *error, gpointer user_data)
         add_network(self);
 }
 
+static void
+assoc_set_pmk_lifetime(GVariant *ret, GError *error, gpointer user_data)
+{
+    NMSupplicantInterface        *self;
+    NMSupplicantInterfacePrivate *priv;
+
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = NM_SUPPLICANT_INTERFACE(user_data);
+    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+    if (error) {
+        assoc_return(self, error, "failure to set PMK lifetime");
+        return;
+    }
+
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: interface PMK lifetime set to %u",
+          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+          PMK_LIFETIME_SEC);
+
+    nm_assert(priv->assoc_data->calls_left > 0);
+    if (--priv->assoc_data->calls_left == 0)
+        add_network(self);
+}
+
 static gboolean
 assoc_fail_on_idle_cb(gpointer user_data)
 {
@@ -2534,6 +2562,21 @@ nm_supplicant_interface_assoc(NMSupplicantInterface       *self,
         assoc_set_ap_scan_cb,
         self);
 
+    /* Set the PMK lifetime to a longer interval (1 week) instead of
+     * the default one (12 hours) that would trigger a WPA-EAP
+     * reauthentication after only 8:24 hours (70% of the lifetime). */
+    assoc_data->calls_left++;
+    nm_dbus_connection_call_set(priv->dbus_connection,
+                                priv->name_owner->str,
+                                priv->object_path->str,
+                                NM_WPAS_DBUS_IFACE_INTERFACE,
+                                "Dot11RSNAConfigPMKLifetime",
+                                g_variant_new_take_string(g_strdup_printf("%u", PMK_LIFETIME_SEC)),
+                                DBUS_TIMEOUT_MSEC,
+                                assoc_data->cancellable,
+                                assoc_set_pmk_lifetime,
+                                self);
+
     ap_isolation = nm_supplicant_config_get_ap_isolation(priv->assoc_data->cfg);
     if (!priv->ap_isolate_supported) {
         if (ap_isolation) {
diff --git a/src/core/supplicant/nm-supplicant-settings-verify.c b/src/core/supplicant/nm-supplicant-settings-verify.c
index 566173af..9881c552 100644
--- a/src/core/supplicant/nm-supplicant-settings-verify.c
+++ b/src/core/supplicant/nm-supplicant-settings-verify.c
@@ -92,6 +92,7 @@ static const struct Opt opt_table[] = {
     OPT_BYTES("mka_cak", 65536),
     OPT_BYTES("mka_ckn", 65536),
     OPT_BYTES("nai", 0),
+    OPT_INT("owe_only", 0, 1),
     OPT_BYTES("pac_file", 0),
     OPT_KEYWORD("pairwise", NM_MAKE_STRV("CCMP", "TKIP", "GCMP-256", "NONE", )),
     OPT_UTF8("password", 0),
diff --git a/src/core/tests/test-core.c b/src/core/tests/test-core.c
index b4e1c4d5..11a7f323 100644
--- a/src/core/tests/test-core.c
+++ b/src/core/tests/test-core.c
@@ -15,7 +15,6 @@
 #include "NetworkManagerUtils.h"
 #include "libnm-core-intern/nm-core-internal.h"
 #include "nm-core-utils.h"
-#include "libnm-systemd-core/nm-sd-utils-core.h"
 
 #include "dns/nm-dns-manager.h"
 #include "nm-connectivity.h"
@@ -2314,7 +2313,6 @@ test_dns_create_resolv_conf(void)
 static void
 test_machine_id_read(void)
 {
-    NMUuid        machine_id_sd;
     const NMUuid *machine_id;
     char          machine_id_str[33];
     gpointer      logstate;
@@ -2346,27 +2344,6 @@ test_machine_id_read(void)
              == machine_id_str);
     g_assert(strlen(machine_id_str) == 32);
     g_assert_cmpstr(machine_id_str, ==, nm_utils_machine_id_str());
-
-    /* double check with systemd's implementation... */
-    if (!nm_sd_utils_id128_get_machine(&machine_id_sd)) {
-        /* if systemd failed to read /etc/machine-id, the file likely
-         * is invalid. Our machine-id is fake, and we have nothing to
-         * compare against. */
-
-        if (g_file_test(LOCALSTATEDIR "/lib/dbus/machine-id", G_FILE_TEST_EXISTS)) {
-            /* Hm. So systemd failed to read /etc/machine-id, but we may have the one from D-Bus.
-             * With LOCALSTATEDIR"/lib/dbus/machine-id", we don't really know whether we
-             * parsed that file. Assume we don't know and skip the test on this system. */
-            g_assert(!nm_utils_machine_id_is_fake());
-            return;
-        }
-
-        /* OK, in this case, our function should have generated a random machine ID. */
-        g_assert(nm_utils_machine_id_is_fake());
-    } else {
-        g_assert(!nm_utils_machine_id_is_fake());
-        g_assert_cmpmem(&machine_id_sd, sizeof(NMUuid), machine_id, 16);
-    }
 }
 
 /*****************************************************************************/
diff --git a/src/libnm-client-impl/nm-client.c b/src/libnm-client-impl/nm-client.c
index b18f47be..8476679b 100644
--- a/src/libnm-client-impl/nm-client.c
+++ b/src/libnm-client-impl/nm-client.c
@@ -531,7 +531,6 @@ _nm_client_dbus_call_simple(NMClient           *self,
     nm_assert(object_path);
     nm_assert(interface_name);
     nm_assert(method_name);
-    nm_assert(parameters);
     nm_assert(reply_type);
 
     dbus_context = nm_g_main_context_push_thread_default_if_necessary(priv->dbus_context);
@@ -592,20 +591,21 @@ _nm_client_dbus_call(NMClient           *self,
     nm_assert(object_path);
     nm_assert(interface_name);
     nm_assert(method_name);
-    nm_assert(parameters);
     nm_assert(reply_type);
 
     task = nm_g_task_new(source_obj, cancellable, source_tag, user_callback, user_callback_data);
 
     if (!self) {
-        nm_g_variant_unref_floating(parameters);
+        if (parameters)
+            nm_g_variant_unref_floating(parameters);
         g_task_return_error(task, _nm_client_new_error_nm_not_cached());
         return;
     }
 
     priv = NM_CLIENT_GET_PRIVATE(self);
     if (!priv->name_owner) {
-        nm_g_variant_unref_floating(parameters);
+        if (parameters)
+            nm_g_variant_unref_floating(parameters);
         g_task_return_error(task, _nm_client_new_error_nm_not_running());
         return;
     }
diff --git a/src/libnm-client-impl/nm-libnm-utils.c b/src/libnm-client-impl/nm-libnm-utils.c
index 951db1bc..6a7c155e 100644
--- a/src/libnm-client-impl/nm-libnm-utils.c
+++ b/src/libnm-client-impl/nm-libnm-utils.c
@@ -99,14 +99,14 @@ _nml_dbus_log(NMLDBusLogLevel level, gboolean use_stdout, const char *fmt, ...)
                 (long long) pid,
                 prefix,
                 ts / NM_UTILS_NSEC_PER_SEC,
-                (ts / (NM_UTILS_NSEC_PER_SEC / 10000)) % 10000,
+                (ts / (NM_UTILS_NSEC_PER_SEC / 100000)) % 100000,
                 msg);
     } else {
         g_printerr("libnm-dbus[%lld]: %s[%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] %s\n",
                    (long long) pid,
                    prefix,
                    ts / NM_UTILS_NSEC_PER_SEC,
-                   (ts / (NM_UTILS_NSEC_PER_SEC / 10000)) % 10000,
+                   (ts / (NM_UTILS_NSEC_PER_SEC / 100000)) % 100000,
                    msg);
     }
 }
diff --git a/src/libnm-core-impl/nm-setting-wireless.c b/src/libnm-core-impl/nm-setting-wireless.c
index 8f57f69b..68cc0e13 100644
--- a/src/libnm-core-impl/nm-setting-wireless.c
+++ b/src/libnm-core-impl/nm-setting-wireless.c
@@ -141,6 +141,10 @@ nm_setting_wireless_ap_security_compatible(NMSettingWireless         *s_wireless
     g_return_val_if_fail(NM_IS_SETTING_WIRELESS(s_wireless), FALSE);
 
     if (!s_wireless_sec) {
+        /* A OWE-TM network can be used w/o security */
+        if (ap_wpa == NM_802_11_AP_SEC_KEY_MGMT_OWE_TM
+            || (ap_rsn == NM_802_11_AP_SEC_KEY_MGMT_OWE_TM))
+            return TRUE;
         if ((ap_flags & NM_802_11_AP_FLAGS_PRIVACY) || (ap_wpa != NM_802_11_AP_SEC_NONE)
             || (ap_rsn != NM_802_11_AP_SEC_NONE))
             return FALSE;
diff --git a/src/libnm-core-impl/tests/certs/test-aes-128-ec-key.pem b/src/libnm-core-impl/tests/certs/test-aes-128-ec-key.pem
new file mode 100644
index 00000000..86989642
--- /dev/null
+++ b/src/libnm-core-impl/tests/certs/test-aes-128-ec-key.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,D6E024FACA9CFFE6B0296EBC848BC80C
+
+vOzvGfSmNKRGMxIlB474A3ZHYqNj0NEuJQxEYV2roY+kAWN4+zdk7I+dbGRbjlgz
+lS0NBIrTEmyPugYEWnaccxKfTTJz5U1OxT/AZVVmJuD/5lh2H2qZUkFs6d10X5zd
+0gsn6OwV+2j9rrWyEcgH8WOwqgiN+LaWihLhGMKaWI0=
+-----END EC PRIVATE KEY-----
diff --git a/src/libnm-core-impl/tests/certs/test-aes-256-ec-key.pem b/src/libnm-core-impl/tests/certs/test-aes-256-ec-key.pem
new file mode 100644
index 00000000..1baf51d3
--- /dev/null
+++ b/src/libnm-core-impl/tests/certs/test-aes-256-ec-key.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,8337BC213C3280DC1884C61562719552
+
+Kcdf9l8gAMkN1fb0PGpFRD1VQIGQbB55yCBLMwRBbG1XQ9rVjnP+Zl0CdDzncKXt
+yLFpAAgNUahsaVJyc8/suKl8pj2VfJK8m0NUIrWWQu/KyaFTS6I30hwLJ0WATBMm
+GMb2CiAqlk6U+EqtZxNmgf6SCEGUV/oTCF0AqNFKiXM=
+-----END EC PRIVATE KEY-----
diff --git a/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.der b/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.der
new file mode 100644
index 00000000..e8d0765a
--- /dev/null
+++ b/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.der
Binary files differdiff --git a/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.pem b/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.pem
new file mode 100644
index 00000000..c40e293b
--- /dev/null
+++ b/src/libnm-core-impl/tests/certs/test-ec-key-only-decrypted.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIPw/e9RgNcs2cMrKn+KnxNCGC1UrcOH9S0xm5e1GxJqRoAoGCCqGSM49
+AwEHoUQDQgAEbA5LlfyjMwuyuIcBr+fk1l71EKC6NttJCGjXGexUOTGsjhVgXHjT
+cc2EohzF94Zs4EjYce3aQe4YsA11YMuRaQ==
+-----END EC PRIVATE KEY-----
diff --git a/src/libnm-core-impl/tests/test-crypto.c b/src/libnm-core-impl/tests/test-crypto.c
index 896c3c2e..fb4c213f 100644
--- a/src/libnm-core-impl/tests/test-crypto.c
+++ b/src/libnm-core-impl/tests/test-crypto.c
@@ -111,7 +111,7 @@ test_load_private_key(const char *path,
 
     array = nmtst_crypto_decrypt_openssl_private_key(path, password, &key_type, &error);
     /* Even if the password is wrong, we should determine the key type */
-    g_assert_cmpint(key_type, ==, NM_CRYPTO_KEY_TYPE_RSA);
+    g_assert_cmpint(key_type, !=, NM_CRYPTO_KEY_TYPE_UNKNOWN);
 
     if (expected_error != -1) {
         g_assert(array == NULL);
@@ -226,7 +226,7 @@ test_encrypt_private_key(const char *path, const char *password)
 
     array = nmtst_crypto_decrypt_openssl_private_key(path, password, &key_type, &error);
     nmtst_assert_success(array, error);
-    g_assert_cmpint(key_type, ==, NM_CRYPTO_KEY_TYPE_RSA);
+    g_assert_cmpint(key_type, !=, NM_CRYPTO_KEY_TYPE_UNKNOWN);
 
     /* Now re-encrypt the private key */
     encrypted = nmtst_crypto_rsa_key_encrypt(g_bytes_get_data(array, NULL),
@@ -244,7 +244,7 @@ test_encrypt_private_key(const char *path, const char *password)
                                                                  &key_type,
                                                                  &error);
     nmtst_assert_success(re_decrypted, error);
-    g_assert_cmpint(key_type, ==, NM_CRYPTO_KEY_TYPE_RSA);
+    g_assert_cmpint(key_type, !=, NM_CRYPTO_KEY_TYPE_UNKNOWN);
 
     /* Compare the original decrypted key with the re-decrypted key */
     g_assert(g_bytes_equal(array, re_decrypted));
@@ -452,12 +452,21 @@ main(int argc, char **argv)
     g_test_add_data_func("/libnm/crypto/key/aes-128",
                          "test-aes-128-key.pem, test-aes-password",
                          test_key);
+    g_test_add_data_func("/libnm/crypto/key/aes-128-ec",
+                         "test-aes-128-ec-key.pem, test-aes-password",
+                         test_key);
     g_test_add_data_func("/libnm/crypto/key/aes-256",
                          "test-aes-256-key.pem, test-aes-password",
                          test_key);
+    g_test_add_data_func("/libnm/crypto/key/aes-256-ec",
+                         "test-aes-256-ec-key.pem, test-aes-password",
+                         test_key);
     g_test_add_data_func("/libnm/crypto/key/decrypted",
                          "test-key-only-decrypted.pem",
                          test_key_decrypted);
+    g_test_add_data_func("/libnm/crypto/key/decrypted-ec",
+                         "test-ec-key-only-decrypted.pem",
+                         test_key_decrypted);
 
     g_test_add_data_func("/libnm/crypto/PKCS#12/1", "test-cert.p12, test", test_pkcs12);
     g_test_add_data_func("/libnm/crypto/PKCS#12/2", "test2-cert.p12, 12345testing", test_pkcs12);
diff --git a/src/libnm-core-public/nm-version-macros.h b/src/libnm-core-public/nm-version-macros.h
index 07c259a7..dc226441 100644
--- a/src/libnm-core-public/nm-version-macros.h
+++ b/src/libnm-core-public/nm-version-macros.h
@@ -30,7 +30,7 @@
  * Evaluates to the micro version number of NetworkManager which this source
  * compiled against.
  */
-#define NM_MICRO_VERSION (0)
+#define NM_MICRO_VERSION (4)
 
 /**
  * NM_CHECK_VERSION:
diff --git a/src/libnm-crypto/nm-crypto.c b/src/libnm-crypto/nm-crypto.c
index 69d2b53f..8cf66336 100644
--- a/src/libnm-crypto/nm-crypto.c
+++ b/src/libnm-crypto/nm-crypto.c
@@ -25,6 +25,9 @@
 #define PEM_DSA_KEY_BEGIN "-----BEGIN DSA PRIVATE KEY-----"
 #define PEM_DSA_KEY_END   "-----END DSA PRIVATE KEY-----"
 
+#define PEM_EC_KEY_BEGIN "-----BEGIN EC PRIVATE KEY-----"
+#define PEM_EC_KEY_END   "-----END EC PRIVATE KEY-----"
+
 #define PEM_CERT_BEGIN "-----BEGIN CERTIFICATE-----"
 #define PEM_CERT_END   "-----END CERTIFICATE-----"
 
@@ -201,6 +204,10 @@ parse_old_openssl_key_file(const guint8       *data,
         key_type  = NM_CRYPTO_KEY_TYPE_DSA;
         start_tag = PEM_DSA_KEY_BEGIN;
         end_tag   = PEM_DSA_KEY_END;
+    } else if (find_tag(PEM_EC_KEY_BEGIN, data, data_len, 0, &start)) {
+        key_type  = NM_CRYPTO_KEY_TYPE_EC;
+        start_tag = PEM_EC_KEY_BEGIN;
+        end_tag   = PEM_EC_KEY_END;
     } else {
         g_set_error(error,
                     _NM_CRYPTO_ERROR,
diff --git a/src/libnm-crypto/nm-crypto.h b/src/libnm-crypto/nm-crypto.h
index 48c7c6b7..289549c6 100644
--- a/src/libnm-crypto/nm-crypto.h
+++ b/src/libnm-crypto/nm-crypto.h
@@ -29,7 +29,8 @@ const NMCryptoCipherInfo *nm_crypto_cipher_get_info_by_name(const char *cipher_n
 typedef enum {
     NM_CRYPTO_KEY_TYPE_UNKNOWN = 0,
     NM_CRYPTO_KEY_TYPE_RSA,
-    NM_CRYPTO_KEY_TYPE_DSA
+    NM_CRYPTO_KEY_TYPE_DSA,
+    NM_CRYPTO_KEY_TYPE_EC,
 } NMCryptoKeyType;
 
 typedef enum {
diff --git a/src/libnm-glib-aux/nm-hash-utils.c b/src/libnm-glib-aux/nm-hash-utils.c
index 68b33998..941aba01 100644
--- a/src/libnm-glib-aux/nm-hash-utils.c
+++ b/src/libnm-glib-aux/nm-hash-utils.c
@@ -45,7 +45,7 @@ again:
             guint8 _extra_entropy[3 * HASH_KEY_SIZE];
         } t_arr;
 
-        nm_utils_random_bytes(&t_arr, sizeof(t_arr));
+        nm_random_get_bytes(&t_arr, sizeof(t_arr));
 
         /* We only initialize one random hash key. So we can spend some effort
          * of getting this right. For one, we collect more random bytes than
diff --git a/src/libnm-glib-aux/nm-random-utils.c b/src/libnm-glib-aux/nm-random-utils.c
index 3a8ba64f..93eee7c4 100644
--- a/src/libnm-glib-aux/nm-random-utils.c
+++ b/src/libnm-glib-aux/nm-random-utils.c
@@ -10,6 +10,7 @@
 #include <fcntl.h>
 #include <sys/auxv.h>
 #include <sys/syscall.h>
+#include <poll.h>
 
 #if USE_SYS_RANDOM_H
 #include <sys/random.h>
@@ -34,18 +35,121 @@
 #define GRND_INSECURE 0x04
 #endif
 
-#if !HAVE_GETRANDOM && defined(SYS_getrandom)
-static int
+#if !HAVE_GETRANDOM
+static ssize_t
 getrandom(void *buf, size_t buflen, unsigned flags)
 {
+#if defined(SYS_getrandom)
     return syscall(SYS_getrandom, buf, buflen, flags);
+#else
+    errno = ENOSYS;
+    return -1;
+#endif
 }
-#undef HAVE_GETRANDOM
-#define HAVE_GETRANDOM 1
 #endif
 
 /*****************************************************************************/
 
+static ssize_t
+_getrandom(void *buf, size_t buflen, unsigned flags)
+{
+    static int have_getrandom = TRUE;
+    ssize_t    l;
+    int        errsv;
+
+    nm_assert(buflen > 0);
+
+    /* This calls getrandom() and either returns the positive
+     * success or an negative errno. ENOSYS means getrandom()
+     * call is not supported. That result is cached and we don't retry. */
+
+    if (!have_getrandom)
+        return -ENOSYS;
+
+    l = getrandom(buf, buflen, flags);
+    if (l > 0)
+        return l;
+    if (l == 0)
+        return -EIO;
+    errsv = errno;
+    if (errsv == ENOSYS)
+        have_getrandom = FALSE;
+    return -errsv;
+}
+
+static ssize_t
+_getrandom_insecure(void *buf, size_t buflen)
+{
+    static int have_grnd_insecure = TRUE;
+    ssize_t    l;
+
+    /* GRND_INSECURE was added recently. We catch EINVAL
+     * if kernel does not support the flag (and cache it). */
+
+    if (!have_grnd_insecure)
+        return -EINVAL;
+
+    l = _getrandom(buf, buflen, GRND_INSECURE);
+
+    if (l == -EINVAL)
+        have_grnd_insecure = FALSE;
+
+    return l;
+}
+
+static ssize_t
+_getrandom_best_effort(void *buf, size_t buflen)
+{
+    ssize_t l;
+
+    /* To get best-effort bytes, we would use GRND_INSECURE (and we try that
+     * first). However, not all kernel versions support that, so we fallback
+     * to GRND_NONBLOCK.
+     *
+     * Granted, this is called from a fallback path where we have no entropy
+     * already, it's unlikely that GRND_NONBLOCK would succeed. Still... */
+    l = _getrandom_insecure(buf, buflen);
+    if (l != -EINVAL)
+        return l;
+
+    return _getrandom(buf, buflen, GRND_NONBLOCK);
+}
+
+static int
+_random_check_entropy(gboolean block)
+{
+    static gboolean   seen_high_quality = FALSE;
+    nm_auto_close int fd                = -1;
+    int               r;
+
+    /* We come here because getrandom() gave ENOSYS. We will fallback to /dev/urandom,
+     * but the caller wants to know whether we have high quality numbers. Poll
+     * /dev/random to find out. */
+
+    if (seen_high_quality) {
+        /* We cache the positive result. Once kernel has entropy, we will get
+         * good random numbers. */
+        return 1;
+    }
+
+    fd = open("/dev/random", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+    if (fd < 0)
+        return -errno;
+
+    r = nm_utils_fd_wait_for_event(fd, POLLIN, block ? -1 : 0);
+
+    if (r <= 0) {
+        nm_assert(r < 0 || !block);
+        return r;
+    }
+
+    nm_assert(r == 1);
+    seen_high_quality = TRUE;
+    return 1;
+}
+
+/*****************************************************************************/
+
 typedef struct _nm_packed {
     uintptr_t heap_ptr;
     uintptr_t stack_ptr;
@@ -72,7 +176,8 @@ typedef struct _nm_packed {
         guint8  u8[NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2];
         guint32 u32[((NM_UTILS_CHECKSUM_LENGTH_SHA256 / 2) + 3) / 4];
     } rand_vals;
-    GRand *rand;
+    guint8 rand_vals_getrandom[16];
+    gint64 rand_vals_timestamp;
 } BadRandState;
 
 static void
@@ -106,18 +211,7 @@ _bad_random_init_seed(BadRandSeed *seed)
         memcpy(&seed->auxval, p_at_random, 16);
     }
 
-#if HAVE_GETRANDOM
-    {
-        ssize_t r;
-
-        /* This is likely to fail, because we already failed a moment earlier. Still, give
-         * it a try. */
-        r = getrandom(seed->getrandom_buf,
-                      sizeof(seed->getrandom_buf),
-                      GRND_INSECURE | GRND_NONBLOCK);
-        (void) r;
-    }
-#endif
+    _getrandom_best_effort(seed->getrandom_buf, sizeof(seed->getrandom_buf));
 
     seed->now_bootime = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
     seed->now_real    = g_get_real_time();
@@ -168,6 +262,10 @@ _bad_random_bytes(guint8 *buf, gsize n)
             nm_utils_checksum_get_digest(sum, gl_state.sha_digest.full);
         }
 
+        _getrandom_best_effort(gl_state.rand_vals_getrandom, sizeof(gl_state.rand_vals_getrandom));
+
+        gl_state.rand_vals_timestamp = nm_utils_clock_gettime_nsec(CLOCK_BOOTTIME);
+
         while (TRUE) {
             int i;
 
@@ -197,103 +295,155 @@ _bad_random_bytes(guint8 *buf, gsize n)
     }
 }
 
+/*****************************************************************************/
+
 /**
- * nm_utils_random_bytes:
+ * nm_random_get_bytes_full:
  * @p: the buffer to fill
  * @n: the number of bytes to write to @p.
+ * @out_high_quality: (allow-none) (out): whether the returned
+ *   random bytes are of high quality.
  *
- * Uses getrandom() or reads /dev/urandom to fill the buffer
- * with random data. If all fails, as last fallback it uses
- * GRand to fill the buffer with pseudo random numbers.
- * The function always succeeds in writing some random numbers
- * to the buffer. The return value of FALSE indicates that the
- * obtained bytes are probably not of good randomness.
- *
- * Returns: whether the written bytes are good. If you
- * don't require good randomness, you can ignore the return
- * value.
- *
- * Note that if calling getrandom() fails because there is not enough
- * entropy (at early boot), the function will read /dev/urandom.
- * Which of course, still has low entropy, and cause kernel to log
- * a warning.
+ * - will never block
+ * - will always produce some numbers, but they may not
+ *   be of high quality.
+ * - Whether they are of high quality, you can know via @out_high_quality.
+ * - will always try hard to produce high quality numbers, and on success
+ *   they are as good as nm_random_get_crypto_bytes().
  */
-gboolean
-nm_utils_random_bytes(void *p, size_t n)
+void
+nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality)
 {
     int      fd;
     int      r;
-    gboolean has_high_quality = TRUE;
-    guint8  *buf              = p;
+    gboolean has_high_quality;
+    ssize_t  l;
 
-    g_return_val_if_fail(p, FALSE);
-    g_return_val_if_fail(n > 0, FALSE);
+    if (n == 0) {
+        NM_SET_OUT(out_high_quality, TRUE);
+        return;
+    }
 
-#if HAVE_GETRANDOM
-    {
-        static gboolean have_syscall = TRUE;
-
-        if (have_syscall) {
-            ssize_t r2;
-            int     errsv;
-
-            r2 = getrandom(buf, n, GRND_NONBLOCK);
-            if (r2 >= 0) {
-                if ((size_t) r2 == n)
-                    return TRUE;
-
-                /* no or partial read. There is not enough entropy.
-                 * Fill the rest reading with the fallback code and remember
-                 * that some bits are not high quality. */
-                nm_assert((size_t) r2 < n);
-                buf += r2;
-                n -= r2;
-
-                /* At this point, we don't want to read /dev/urandom, because
-                 * the entropy pool is low (early boot?), and asking for more
-                 * entropy causes kernel messages to be logged.
-                 *
-                 * Note that we fall back to _bad_random_bytes(), which (among others) seeds
-                 * itself with g_rand_new(). That also will read /dev/urandom, but as
-                 * we do that only once, we don't care. But in general, we are here in
-                 * a situation where we want to avoid reading /dev/urandom too much. */
-                goto out_bad_random;
-            }
-            errsv = errno;
-            if (errsv == ENOSYS) {
-                /* no support for getrandom(). We don't know whether
-                 * we /dev/urandom will give us good quality. Assume yes. */
-                have_syscall = FALSE;
-            } else if (errsv == EAGAIN) {
-                /* No entropy. We avoid reading /dev/urandom. */
-                goto out_bad_random;
-            } else {
-                /* Unknown error, likely no entropy. We'll read /dev/urandom below, but we don't
-                 * have high-quality randomness. */
-                has_high_quality = FALSE;
+    g_return_if_fail(p);
+
+again_getrandom:
+    l = _getrandom(p, n, GRND_NONBLOCK);
+    if (l > 0) {
+        if ((size_t) l == n) {
+            NM_SET_OUT(out_high_quality, TRUE);
+            return;
+        }
+        p = ((uint8_t *) p) + l;
+        n -= l;
+        goto again_getrandom;
+    }
+
+    /* getrandom() failed. Fallback to read /dev/urandom. */
+
+    if (l == -ENOSYS) {
+        /* no support for getrandom(). */
+        if (out_high_quality) {
+            /* The caller wants to know whether we have high quality. Poll /dev/random
+             * to find out. */
+            has_high_quality = (_random_check_entropy(FALSE) > 0);
+        } else {
+            /* The value doesn't matter in this case. It will be unused. */
+            has_high_quality = FALSE;
+        }
+    } else {
+        /* Any other failure of getrandom() means we don't have high quality. */
+        has_high_quality = FALSE;
+        if (l == -EAGAIN) {
+            /* getrandom(GRND_NONBLOCK) failed because lack of entropy. Retry with GRND_INSECURE. */
+            for (;;) {
+                l = _getrandom_insecure(p, n);
+                if (l > 0) {
+                    if ((size_t) l == n) {
+                        NM_SET_OUT(out_high_quality, FALSE);
+                        return;
+                    }
+                    p = ((uint8_t *) p) + l;
+                    n -= l;
+                    continue;
+                }
+                /* Any error. Fallback to /dev/urandom. */
+                break;
             }
         }
     }
-#endif
 
-fd_open:
+again_open:
     fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
     if (fd < 0) {
         if (errno == EINTR)
-            goto fd_open;
-        goto out_bad_random;
+            goto again_open;
+    } else {
+        r = nm_utils_fd_read_loop_exact(fd, p, n, TRUE);
+        nm_close(fd);
+        if (r >= 0) {
+            NM_SET_OUT(out_high_quality, has_high_quality);
+            return;
+        }
     }
-    r = nm_utils_fd_read_loop_exact(fd, buf, n, TRUE);
-    nm_close(fd);
-    if (r >= 0)
-        return has_high_quality;
 
-out_bad_random:
     /* we failed to fill the bytes reading from /dev/urandom.
-     * Fill the bits using our pseudo random numbers.
-     *
-     * We don't have good quality.
+     * Fill the bits using our fallback approach (which obviously
+     * cannot give high quality random).
      */
-    _bad_random_bytes(buf, n);
-    return FALSE;
+    _bad_random_bytes(p, n);
+    NM_SET_OUT(out_high_quality, FALSE);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_random_get_crypto_bytes:
+ * @p: the buffer to fill
+ * @n: the number of bytes to fill
+ *
+ * - can fail (in which case a negative number is returned
+ *   and the output buffer is undefined).
+ * - will block trying to get high quality random numbers.
+ */
+int
+nm_random_get_crypto_bytes(void *p, size_t n)
+{
+    nm_auto_close int fd = -1;
+    ssize_t           l;
+    int               r;
+
+    if (n == 0)
+        return 0;
+
+    nm_assert(p);
+
+again_getrandom:
+    l = _getrandom(p, n, 0);
+    if (l > 0) {
+        if ((size_t) l == n)
+            return 0;
+        p = (uint8_t *) p + l;
+        n -= l;
+        goto again_getrandom;
+    }
+
+    if (l != -ENOSYS) {
+        /* We got a failure, but getrandom seems to be working in principle. We
+         * won't get good numbers. Fail. */
+        return l;
+    }
+
+    /* getrandom() failed with ENOSYS. Fallback to reading /dev/urandom. */
+
+    r = _random_check_entropy(TRUE);
+    if (r < 0)
+        return r;
+    if (r == 0)
+        return nm_assert_unreachable_val(-EIO);
+
+    fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOCTTY);
+    if (fd < 0)
+        return -errno;
+
+    return nm_utils_fd_read_loop_exact(fd, p, n, FALSE);
 }
diff --git a/src/libnm-glib-aux/nm-random-utils.h b/src/libnm-glib-aux/nm-random-utils.h
index d0eae103..ab8aee1b 100644
--- a/src/libnm-glib-aux/nm-random-utils.h
+++ b/src/libnm-glib-aux/nm-random-utils.h
@@ -6,6 +6,14 @@
 #ifndef __NM_RANDOM_UTILS_H__
 #define __NM_RANDOM_UTILS_H__
 
-gboolean nm_utils_random_bytes(void *p, size_t n);
+void nm_random_get_bytes_full(void *p, size_t n, gboolean *out_high_quality);
+
+static inline void
+nm_random_get_bytes(void *p, size_t n)
+{
+    nm_random_get_bytes_full(p, n, NULL);
+}
+
+int nm_random_get_crypto_bytes(void *p, size_t n);
 
 #endif /* __NM_RANDOM_UTILS_H__ */
diff --git a/src/libnm-glib-aux/nm-shared-utils.c b/src/libnm-glib-aux/nm-shared-utils.c
index ad99a6b9..f770b96d 100644
--- a/src/libnm-glib-aux/nm-shared-utils.c
+++ b/src/libnm-glib-aux/nm-shared-utils.c
@@ -37,6 +37,13 @@ const void *const _NM_PTRARRAY_EMPTY[1] = {NULL};
 
 const NMIPAddr nm_ip_addr_zero = {};
 
+/* We use _nm_alignas(NMIPAddr) to ensure that fields for in_addr_t and
+ * struct in6_addr have all the same alignment. Ensure that this is suitable. */
+G_STATIC_ASSERT(_nm_alignof(in_addr_t) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in6_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) <= _nm_alignof(NMIPAddr));
+
 /* this initializes a struct in_addr/in6_addr and allows for untrusted
  * arguments (like unsuitable @addr_family or @src_len). It's almost safe
  * in the sense that it verifies input arguments strictly. Also, it
@@ -126,6 +133,9 @@ nm_ip_addr_set_from_variant(int addr_family, gpointer dst, GVariant *variant, in
 
 G_STATIC_ASSERT(ETH_ALEN == sizeof(struct ether_addr));
 G_STATIC_ASSERT(ETH_ALEN == 6);
+G_STATIC_ASSERT(ETH_ALEN == sizeof(NMEtherAddr));
+
+G_STATIC_ASSERT(_nm_alignof(struct ether_addr) <= _nm_alignof(NMEtherAddr));
 
 /*****************************************************************************/
 
@@ -3337,6 +3347,8 @@ nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec)
     struct timespec ts, *pts;
     int             r;
 
+    nm_assert(fd >= 0);
+
     if (timeout_nsec < 0)
         pts = NULL;
     else {
@@ -3350,6 +3362,13 @@ nm_utils_fd_wait_for_event(int fd, int event, gint64 timeout_nsec)
         return -NM_ERRNO_NATIVE(errno);
     if (r == 0)
         return 0;
+
+    nm_assert(r == 1);
+    nm_assert(pollfd.revents > 0);
+
+    if (pollfd.revents & POLLNVAL)
+        return nm_assert_unreachable_val(-EBADF);
+
     return pollfd.revents;
 }
 
diff --git a/src/libnm-glib-aux/nm-shared-utils.h b/src/libnm-glib-aux/nm-shared-utils.h
index daefa068..0589eeac 100644
--- a/src/libnm-glib-aux/nm-shared-utils.h
+++ b/src/libnm-glib-aux/nm-shared-utils.h
@@ -216,7 +216,7 @@ nm_ether_addr_equal(const NMEtherAddr *a, const NMEtherAddr *b)
 
 typedef struct {
     union {
-        guint8          addr_ptr[1];
+        guint8          addr_ptr[sizeof(struct in6_addr)];
         in_addr_t       addr4;
         struct in_addr  addr4_struct;
         struct in6_addr addr6;
@@ -623,6 +623,16 @@ nm_utils_is_separator(const char c)
 
 /*****************************************************************************/
 
+static inline GBytes *
+nm_g_bytes_ref(GBytes *b)
+{
+    if (b)
+        g_bytes_ref(b);
+    return b;
+}
+
+/*****************************************************************************/
+
 GBytes *nm_g_bytes_get_empty(void);
 
 GBytes *nm_g_bytes_new_from_str(const char *str);
@@ -2311,6 +2321,25 @@ nm_g_hash_table_contains(GHashTable *hash, gconstpointer key)
     return hash ? g_hash_table_contains(hash, key) : FALSE;
 }
 
+#define nm_g_hash_table_contains_any(hash, ...)                              \
+    ({                                                                       \
+        GHashTable *const   _hash   = (hash);                                \
+        gconstpointer const _keys[] = {__VA_ARGS__};                         \
+        int                 _i_key;                                          \
+        gboolean            _contains = FALSE;                               \
+                                                                             \
+        if (_hash) {                                                         \
+            for (_i_key = 0; _i_key < (int) G_N_ELEMENTS(_keys); _i_key++) { \
+                if (g_hash_table_contains(_hash, _keys[_i_key])) {           \
+                    _contains = TRUE;                                        \
+                    break;                                                   \
+                }                                                            \
+            }                                                                \
+        }                                                                    \
+                                                                             \
+        _contains;                                                           \
+    })
+
 static inline gboolean
 nm_g_hash_table_remove(GHashTable *hash, gconstpointer key)
 {
diff --git a/src/libnm-glib-aux/nm-uuid.c b/src/libnm-glib-aux/nm-uuid.c
index ff0649d0..464bbf58 100644
--- a/src/libnm-glib-aux/nm-uuid.c
+++ b/src/libnm-glib-aux/nm-uuid.c
@@ -115,12 +115,12 @@ nm_uuid_generate_random(NMUuid *out_uuid)
 
     /* See also, systemd's id128_make_v4_uuid() */
 
-    /* nm_utils_random_bytes() is supposed to try hard to give good
+    /* nm_random_get_bytes() is supposed to try hard to give good
      * randomness. If it fails, it still makes an effort to fill
      * random data into the buffer. There is not much we can do about
      * that case, except making sure that it does not happen in the
      * first place. */
-    nm_utils_random_bytes(out_uuid, sizeof(*out_uuid));
+    nm_random_get_bytes(out_uuid, sizeof(*out_uuid));
 
     /* Set the four most significant bits (bits 12 through 15) of the
      * time_hi_and_version field to the 4-bit version number from
diff --git a/src/libnm-glib-aux/tests/test-shared-general.c b/src/libnm-glib-aux/tests/test-shared-general.c
index 46689947..9acd535d 100644
--- a/src/libnm-glib-aux/tests/test-shared-general.c
+++ b/src/libnm-glib-aux/tests/test-shared-general.c
@@ -23,10 +23,10 @@ G_STATIC_ASSERT(NM_AF_INET_SIZE == sizeof(in_addr_t));
 G_STATIC_ASSERT(NM_AF_INET_SIZE == sizeof(struct in_addr));
 G_STATIC_ASSERT(NM_AF_INET6_SIZE == sizeof(struct in6_addr));
 
-G_STATIC_ASSERT(4 == _nm_alignof(in_addr_t));
-G_STATIC_ASSERT(4 == _nm_alignof(struct in_addr));
-G_STATIC_ASSERT(4 == _nm_alignof(struct in6_addr));
-G_STATIC_ASSERT(4 == _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(in_addr_t) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(struct in6_addr) <= _nm_alignof(NMIPAddr));
+G_STATIC_ASSERT(_nm_alignof(NMEtherAddr) <= _nm_alignof(NMIPAddr));
 
 /*****************************************************************************/
 
@@ -93,7 +93,7 @@ test_nmhash(void)
 {
     int rnd;
 
-    nm_utils_random_bytes(&rnd, sizeof(rnd));
+    nm_random_get_bytes(&rnd, sizeof(rnd));
 
     g_assert(nm_hash_val(555, 4) != 0);
 }
diff --git a/src/libnm-platform/nm-platform.h b/src/libnm-platform/nm-platform.h
index 5c72fade..190a91dc 100644
--- a/src/libnm-platform/nm-platform.h
+++ b/src/libnm-platform/nm-platform.h
@@ -333,6 +333,9 @@ typedef enum {
      * should be configured. */             \
     bool a_force_commit : 1;                                                                 \
                                                                                              \
+    /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
+     * be unclear how the following fields get merged. We could also use a zero bitfield,
+     * but instead we just have there the uint8 field. */    \
     guint8 plen;                                                                             \
     ;
 
@@ -343,10 +346,7 @@ typedef enum {
  **/
 typedef struct {
     __NMPlatformIPAddress_COMMON;
-    union {
-        guint8  address_ptr[1];
-        guint32 __dummy_for_32bit_alignment;
-    };
+    _nm_alignas(NMIPAddr) guint8 address_ptr[];
 } NMPlatformIPAddress;
 
 /**
@@ -356,13 +356,8 @@ typedef struct {
 struct _NMPlatformIP4Address {
     __NMPlatformIPAddress_COMMON;
 
-    /* Whether the address is ready to be configured. By default, an address is, but this
-     * flag may indicate that the address is just for tracking purpose only, but the ACD
-     * state is not yet ready for the address to be configured. */
-    bool a_acd_not_ready : 1;
-
     /* The local address IFA_LOCAL. */
-    in_addr_t address;
+    _nm_alignas(NMIPAddr) in_addr_t address;
 
     /* The IFA_ADDRESS PTP peer address. This field is rather important, because
      * it constitutes the identifier for the IPv4 address (e.g. you can add two
@@ -382,6 +377,11 @@ struct _NMPlatformIP4Address {
     in_addr_t broadcast_address;
 
     char label[NMP_IFNAMSIZ];
+
+    /* Whether the address is ready to be configured. By default, an address is, but this
+     * flag may indicate that the address is just for tracking purpose only, but the ACD
+     * state is not yet ready for the address to be configured. */
+    bool a_acd_not_ready : 1;
 };
 
 /**
@@ -390,7 +390,7 @@ struct _NMPlatformIP4Address {
  **/
 struct _NMPlatformIP6Address {
     __NMPlatformIPAddress_COMMON;
-    struct in6_addr address;
+    _nm_alignas(NMIPAddr) struct in6_addr address;
     struct in6_addr peer_address;
 };
 
@@ -426,6 +426,47 @@ typedef union {
 #define __NMPlatformIPRoute_COMMON                                                        \
     __NMPlatformObjWithIfindex_COMMON;                                                    \
                                                                                           \
+    /* rtnh_flags
+     *
+     * Routes with rtm_flags RTM_F_CLONED are hidden by platform and
+     * do not exist from the point-of-view of platform users.
+     * Such a route is not alive, according to nmp_object_is_alive().
+     *
+     * NOTE: currently we ignore all flags except RTM_F_CLONED
+     * and RTNH_F_ONLINK.
+     * We also may not properly consider the flags as part of the ID
+     * in route-cmp. */                                                                         \
+    unsigned r_rtm_flags;                                                                 \
+                                                                                          \
+    /* RTA_METRICS.RTAX_ADVMSS (iproute2: advmss) */                                      \
+    guint32 mss;                                                                          \
+                                                                                          \
+    /* RTA_METRICS.RTAX_WINDOW (iproute2: window) */                                      \
+    guint32 window;                                                                       \
+                                                                                          \
+    /* RTA_METRICS.RTAX_CWND (iproute2: cwnd) */                                          \
+    guint32 cwnd;                                                                         \
+                                                                                          \
+    /* RTA_METRICS.RTAX_INITCWND (iproute2: initcwnd) */                                  \
+    guint32 initcwnd;                                                                     \
+                                                                                          \
+    /* RTA_METRICS.RTAX_INITRWND (iproute2: initrwnd) */                                  \
+    guint32 initrwnd;                                                                     \
+                                                                                          \
+    /* RTA_METRICS.RTAX_MTU (iproute2: mtu) */                                            \
+    guint32 mtu;                                                                          \
+                                                                                          \
+    /* RTA_PRIORITY (iproute2: metric)
+     * If "metric_any" is %TRUE, then this is interpreted as an offset that will be
+     * added to a default base metric. In such cases, the offset is usually zero. */                                                    \
+    guint32 metric;                                                                       \
+                                                                                          \
+    /* rtm_table, RTA_TABLE.
+     *
+     * This is not the original table ID. Instead, 254 (RT_TABLE_MAIN) and
+     * zero (RT_TABLE_UNSPEC) are swapped, so that the default is the main
+     * table. Use nm_platform_route_table_coerce()/nm_platform_route_table_uncoerce(). */                                                              \
+    guint32 table_coerced;                                                                \
     /* The NMIPConfigSource. For routes that we receive from cache this corresponds
      * to the rtm_protocol field (and is one of the NM_IP_CONFIG_SOURCE_RTPROT_* values).
      * When adding a route, the source will be coerced to the protocol using
@@ -440,8 +481,6 @@ typedef union {
      * to zero, in which case the first matching route (with proto ignored) is deleted. */       \
     NMIPConfigSource rt_source;                                                           \
                                                                                           \
-    guint8 plen;                                                                          \
-                                                                                          \
     /* RTA_METRICS:
      *
      * For IPv4 routes, these properties are part of their
@@ -480,62 +519,22 @@ typedef union {
     /* Whether the route should be committed even if it was removed externally. */        \
     bool r_force_commit : 1;                                                              \
                                                                                           \
-    /* rtnh_flags
-     *
-     * Routes with rtm_flags RTM_F_CLONED are hidden by platform and
-     * do not exist from the point-of-view of platform users.
-     * Such a route is not alive, according to nmp_object_is_alive().
-     *
-     * NOTE: currently we ignore all flags except RTM_F_CLONED
-     * and RTNH_F_ONLINK.
-     * We also may not properly consider the flags as part of the ID
-     * in route-cmp. */                                                                         \
-    unsigned r_rtm_flags;                                                                 \
-                                                                                          \
-    /* RTA_METRICS.RTAX_ADVMSS (iproute2: advmss) */                                      \
-    guint32 mss;                                                                          \
-                                                                                          \
-    /* RTA_METRICS.RTAX_WINDOW (iproute2: window) */                                      \
-    guint32 window;                                                                       \
-                                                                                          \
-    /* RTA_METRICS.RTAX_CWND (iproute2: cwnd) */                                          \
-    guint32 cwnd;                                                                         \
-                                                                                          \
-    /* RTA_METRICS.RTAX_INITCWND (iproute2: initcwnd) */                                  \
-    guint32 initcwnd;                                                                     \
-                                                                                          \
-    /* RTA_METRICS.RTAX_INITRWND (iproute2: initrwnd) */                                  \
-    guint32 initrwnd;                                                                     \
-                                                                                          \
-    /* RTA_METRICS.RTAX_MTU (iproute2: mtu) */                                            \
-    guint32 mtu;                                                                          \
-                                                                                          \
-    /* RTA_PRIORITY (iproute2: metric)
-     * If "metric_any" is %TRUE, then this is interpreted as an offset that will be
-     * added to a default base metric. In such cases, the offset is usually zero. */                                                    \
-    guint32 metric;                                                                       \
-                                                                                          \
-    /* rtm_table, RTA_TABLE.
-     *
-     * This is not the original table ID. Instead, 254 (RT_TABLE_MAIN) and
-     * zero (RT_TABLE_UNSPEC) are swapped, so that the default is the main
-     * table. Use nm_platform_route_table_coerce()/nm_platform_route_table_uncoerce(). */                                                              \
-    guint32 table_coerced;                                                                \
-                                                                                          \
     /* rtm_type.
      *
      * This is not the original type, if type_coerced is 0 then
      * it means RTN_UNSPEC otherwise the type value is preserved.
-     * */                                                                          \
+     */                                                                          \
     guint8 type_coerced;                                                                  \
+                                                                                          \
+    /* Don't have a bitfield as last field in __NMPlatformIPAddress_COMMON. It would then
+     * be unclear how the following fields get merged. We could also use a zero bitfield,
+     * but instead we just have there the uint8 field. */ \
+    guint8 plen;                                                                          \
     ;
 
 typedef struct {
     __NMPlatformIPRoute_COMMON;
-    union {
-        guint8  network_ptr[1];
-        guint32 __dummy_for_32bit_alignment;
-    };
+    _nm_alignas(NMIPAddr) guint8 network_ptr[];
 } NMPlatformIPRoute;
 
 #define NM_PLATFORM_IP_ROUTE_CAST(route) \
diff --git a/src/libnm-systemd-core/meson.build b/src/libnm-systemd-core/meson.build
index 80b95c42..8bbc02a7 100644
--- a/src/libnm-systemd-core/meson.build
+++ b/src/libnm-systemd-core/meson.build
@@ -25,7 +25,6 @@ libnm_systemd_core = static_library(
     'src/libsystemd/sd-id128/id128-util.c',
     'src/libsystemd/sd-id128/sd-id128.c',
     'nm-sd.c',
-    'nm-sd-utils-core.c',
     'nm-sd-utils-dhcp.c',
     'sd-adapt-core/nm-sd-adapt-core.c',
   ),
diff --git a/src/libnm-systemd-core/nm-sd-utils-core.c b/src/libnm-systemd-core/nm-sd-utils-core.c
deleted file mode 100644
index 21e8a304..00000000
--- a/src/libnm-systemd-core/nm-sd-utils-core.c
+++ /dev/null
@@ -1,27 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2018 Red Hat, Inc.
- */
-
-#include "libnm-systemd-core/nm-default-systemd-core.h"
-
-#include "nm-sd-utils-core.h"
-
-#include "libnm-glib-aux/nm-uuid.h"
-
-#include "nm-sd-adapt-core.h"
-
-#include "sd-id128.h"
-
-/*****************************************************************************/
-
-NMUuid *
-nm_sd_utils_id128_get_machine(NMUuid *out_uuid)
-{
-    g_assert(out_uuid);
-
-    G_STATIC_ASSERT_EXPR(sizeof(*out_uuid) == sizeof(sd_id128_t));
-    if (sd_id128_get_machine((sd_id128_t *) out_uuid) < 0)
-        return NULL;
-    return out_uuid;
-}
diff --git a/src/libnm-systemd-core/nm-sd-utils-core.h b/src/libnm-systemd-core/nm-sd-utils-core.h
deleted file mode 100644
index ccad0029..00000000
--- a/src/libnm-systemd-core/nm-sd-utils-core.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2018 Red Hat, Inc.
- */
-
-#ifndef __NM_SD_UTILS_CORE_H__
-#define __NM_SD_UTILS_CORE_H__
-
-/*****************************************************************************/
-
-struct _NMUuid;
-
-struct _NMUuid *nm_sd_utils_id128_get_machine(struct _NMUuid *out_uuid);
-
-/*****************************************************************************/
-
-#endif /* __NM_SD_UTILS_CORE_H__ */
diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c
index 6fce9de7..be51731f 100644
--- a/src/nmcli/devices.c
+++ b/src/nmcli/devices.c
@@ -1343,9 +1343,10 @@ fill_output_access_point(NMAccessPoint *ap, const APInfo *info)
         if (rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_SAE) {
             g_string_append(security_str, "WPA3 ");
         }
-        if (NM_FLAGS_ANY(rsn_flags,
-                         NM_802_11_AP_SEC_KEY_MGMT_OWE | NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) {
+        if (NM_FLAGS_ANY(rsn_flags, NM_802_11_AP_SEC_KEY_MGMT_OWE)) {
             g_string_append(security_str, "OWE ");
+        } else if (NM_FLAGS_ANY(rsn_flags, NM_802_11_AP_SEC_KEY_MGMT_OWE_TM)) {
+            g_string_append(security_str, "OWE-TM ");
         }
         if ((wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)
             || (rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) {
diff --git a/src/nmcli/nmcli-completion b/src/nmcli/nmcli-completion
index 83ec1e3c..f2de7b67 100644
--- a/src/nmcli/nmcli-completion
+++ b/src/nmcli/nmcli-completion
@@ -84,7 +84,7 @@ _nmcli()
             # no quotes in front, escaping _everything_
             # [ ]bla'bla"bla\bla bla --> [ ]bla\'bla\"bla\\bla\ bla
             entry="${entry//\\/\\\\}"
-            entry="${entry//\'/\'}"
+            entry="${entry//\'/\\\'}"
             entry="${entry//\"/\\\"}"
             entry="${entry// /\\ }"
             entry="${entry//\(/\\(}"