about summary refs log tree commit diff
path: root/src/core
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-08-12 19:21:11 +0200
committerMichael Biebl <biebl@debian.org>2022-08-12 19:21:11 +0200
commit6accbd3ec0e42d8633bbde4d47ed7bfe854e7e0b (patch)
treea77a0617213a5183a4de43824e3fbae9779e9b8e /src/core
parentf7c750061dab327e638c0129cfafd5b2215b3a2e (diff)
New upstream version 1.38.4 upstream/1.38.4
Diffstat (limited to 'src/core')
-rw-r--r--src/core/devices/nm-device-bridge.c3
-rw-r--r--src/core/devices/nm-device.c37
-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.c41
-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-policy.c17
-rw-r--r--src/core/platform/tests/test-route.c5
-rw-r--r--src/core/ppp/nm-ppp-manager.c2
-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
26 files changed, 791 insertions, 470 deletions
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.c b/src/core/devices/nm-device.c
index 82ce712a..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:
@@ -7857,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));
 
@@ -7875,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
@@ -11382,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);
 }
@@ -11623,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) {
@@ -11887,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,
@@ -12682,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 2572213c..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);
 
@@ -2512,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)) {
@@ -2584,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,
@@ -2636,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);
@@ -3743,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-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-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 5c2b7681..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,
@@ -661,7 +660,6 @@ impl_ppp_manager_set_ip6_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_INET6, 0);
 
     address = (NMPlatformIP6Address){
         .plen        = 64,
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),