summary refs log tree commit diff
path: root/src/core/dhcp
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2022-08-16 18:24:19 +0200
committerMichael Biebl <biebl@debian.org>2022-08-16 18:24:19 +0200
commit0018d1f3cf71d680d7b6bceda55a5717244d8b26 (patch)
treea058f1d106d172d3354179437ef034c9355cdf9c /src/core/dhcp
parent6accbd3ec0e42d8633bbde4d47ed7bfe854e7e0b (diff)
New upstream version 1.39.90 upstream/1.39.90
Diffstat (limited to 'src/core/dhcp')
-rw-r--r--src/core/dhcp/nm-dhcp-client.c745
-rw-r--r--src/core/dhcp/nm-dhcp-client.h8
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c125
-rw-r--r--src/core/dhcp/nm-dhcp-manager.c59
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c315
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c725
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c197
-rw-r--r--src/core/dhcp/nm-dhcp-utils.h51
-rw-r--r--src/core/dhcp/tests/test-dhcp-utils.c48
9 files changed, 1194 insertions, 1079 deletions
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 00a2d207..77cfeecf 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -32,6 +32,38 @@
 
 /*****************************************************************************/
 
+/* This is how long we do ACD for each entry and reject new offers for
+ * the same address. Note that the maximum ACD timeout is limited to 30 seconds
+ * (NM_ACD_TIMEOUT_MAX_MSEC).
+ **/
+#define ACD_REGLIST_GRACE_PERIOD_MSEC 300000u
+
+G_STATIC_ASSERT(ACD_REGLIST_GRACE_PERIOD_MSEC > (NM_ACD_TIMEOUT_MAX_MSEC + 1000));
+
+#define ACD_REGLIST_MAX_ENTRIES 30
+
+/* To do ACD for an address (new lease), we will register a NML3ConfigData
+ * with l3cfg. After ACD completes, we still continue having NML3Cfg
+ * watch that address, for ACD_REGLIST_GRACE_PERIOD_MSEC. The reasons are:
+ *
+ * - the caller is supposed to actually configure the address right after
+ *   ACD passed. We would not want to drop the ACD state before the caller
+ *   got a chance to do that.
+ * - when ACD fails, we decline the address and expect the DHCP client
+ *   to present a new lease. We may want to outright reject the address,
+ *   if ACD is bad. Thus, we want to keep running ACD for the address a bit
+ *   longer, so that future requests for the same address can be rejected.
+ *
+ * This data structure is used for tracking the registered ACD address.
+ */
+typedef struct {
+    const NML3ConfigData *l3cd;
+    gint64                expiry_msec;
+    in_addr_t             addr;
+} AcdRegListData;
+
+/*****************************************************************************/
+
 enum {
     SIGNAL_NOTIFY,
     LAST_SIGNAL,
@@ -42,15 +74,48 @@ static guint signals[LAST_SIGNAL] = {0};
 NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient, PROP_CONFIG, );
 
 typedef struct _NMDhcpClientPrivate {
-    NMDhcpClientConfig    config;
-    const NML3ConfigData *l3cd;
-    GSource              *no_lease_timeout_source;
-    GSource              *watch_source;
-    GBytes               *effective_client_id;
+    NMDhcpClientConfig config;
+
+    /* This is the "next" data. That is, the one what was received last via
+     * _nm_dhcp_client_notify(), but which is currently pending on ACD. */
+    const NML3ConfigData *l3cd_next;
+
+    /* This is the currently exposed data. It passed ACD (or no ACD was performed),
+     * and is set from l3cd_next. */
+    const NML3ConfigData *l3cd_curr;
+
+    GSource *no_lease_timeout_source;
+    GSource *watch_source;
+    GBytes  *effective_client_id;
 
     union {
         struct {
             struct {
+                NML3CfgCommitTypeHandle *l3cfg_commit_handle;
+                GSource                 *done_source;
+
+                /* When we do ACD for a l3cd lease, we will keep running ACD for
+                 * the grace period ACD_REGLIST_GRACE_PERIOD_MSEC, even if we already
+                 * determined the state. There are two reasons for that:
+                 *
+                 * - after ACD completes we notify the lease to the user, who is supposed
+                 *   to configure the address in NML3Cfg. If we were already removing the
+                 *   ACD state from NML3Cfg, ACD might need to start over. Instead, when
+                 *   the caller tries to configure the address, ACD state is already good.
+                 *
+                 * - if we decline on ACD offer, we may want to keep running and
+                 *   select other offers. Offers for which we just failed ACD (within
+                 *   ACD_REGLIST_GRACE_PERIOD_MSEC) are rejected. See _nm_dhcp_client_accept_offer().
+                 *   For that, we keep monitoring the ACD state for up to ACD_REGLIST_MAX_ENTRIES
+                 *   addresses, to not restart and select the same lease twice in a row.
+                 */
+                GArray  *reglist;
+                GSource *reglist_timeout_source;
+
+                in_addr_t    addr;
+                NMOptionBool state;
+            } acd;
+            struct {
                 GDBusMethodInvocation *invocation;
             } bound;
         } v4;
@@ -77,16 +142,22 @@ G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+#define L3CD_ACD_TAG(priv) (&(priv)->v4.acd.addr)
+
 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 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);
 
+static void _acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec);
+
+static void _acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log);
+
 /*****************************************************************************/
 
 /* we use pid=-1 for invalid PIDs. Ensure that pid_t can hold negative values. */
@@ -157,6 +228,16 @@ nm_dhcp_client_get_effective_client_id(NMDhcpClient *self)
     return priv->effective_client_id;
 }
 
+NML3ConfigData *
+nm_dhcp_client_create_l3cd(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return nm_l3_config_data_new(nm_l3cfg_get_multi_idx(priv->config.l3cfg),
+                                 nm_l3cfg_get_ifindex(priv->config.l3cfg),
+                                 NM_IP_CONFIG_SOURCE_DHCP);
+}
+
 /*****************************************************************************/
 
 void
@@ -200,7 +281,8 @@ l3_cfg_notify_check_connected(NMDhcpClient *self)
     gboolean             do_connect;
 
     do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address
-                 | priv->l3cfg_notify.wait_ipv6_dad;
+                 | priv->l3cfg_notify.wait_ipv6_dad
+                 | (NM_IS_IPv4(priv->config.addr_family) && priv->v4.acd.l3cfg_commit_handle);
 
     if (!do_connect) {
         nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id);
@@ -306,6 +388,333 @@ _no_lease_timeout_schedule(NMDhcpClient *self)
 
 /*****************************************************************************/
 
+static void
+_acd_state_reset(NMDhcpClient *self, gboolean forget_addr, gboolean forget_reglist)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return;
+
+    if (priv->v4.acd.addr != INADDR_ANY) {
+        nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle);
+        l3_cfg_notify_check_connected(self);
+        nm_clear_g_source_inst(&priv->v4.acd.done_source);
+        if (forget_addr) {
+            priv->v4.acd.addr  = INADDR_ANY;
+            priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT;
+        }
+    } else
+        nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+
+    if (forget_reglist) {
+        guint n;
+
+        while ((n = nm_g_array_len(priv->v4.acd.reglist)) > 0)
+            _acd_reglist_data_remove(self, n - 1, TRUE);
+    }
+
+    nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(!priv->v4.acd.done_source);
+    nm_assert(!forget_reglist
+              || !nm_l3cfg_remove_config_all(priv->config.l3cfg, L3CD_ACD_TAG(priv)));
+}
+
+static gboolean
+_acd_complete_on_idle_cb(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_assert(NM_IS_IPv4(priv->config.addr_family));
+    nm_assert(priv->v4.acd.addr != INADDR_ANY);
+    nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(priv->l3cd_next);
+
+    _acd_state_reset(self, FALSE, FALSE);
+
+    _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_BOUND, priv->l3cd_next);
+
+    return G_SOURCE_CONTINUE;
+}
+
+#define _acd_reglist_data_get(priv, idx) \
+    nm_g_array_index_p((priv)->v4.acd.reglist, AcdRegListData, (idx))
+
+static guint
+_acd_reglist_data_find(NMDhcpClientPrivate *priv, in_addr_t addr_needle)
+{
+    const guint n = nm_g_array_len(priv->v4.acd.reglist);
+    guint       i;
+
+    nm_assert(addr_needle != INADDR_ANY);
+
+    for (i = 0; i < n; i++) {
+        AcdRegListData *reglist_data = _acd_reglist_data_get(priv, i);
+
+        if (reglist_data->addr == addr_needle)
+            return i;
+    }
+    return G_MAXUINT;
+}
+
+static void
+_acd_reglist_data_remove(NMDhcpClient *self, guint idx, gboolean do_log)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    AcdRegListData      *reglist_data;
+
+    nm_assert(idx < nm_g_array_len(priv->v4.acd.reglist));
+
+    reglist_data = _acd_reglist_data_get(priv, idx);
+
+    if (do_log) {
+        char sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+
+        _LOGD("acd: drop check for address %s (l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")",
+              _nm_utils_inet4_ntop(reglist_data->addr, sbuf_addr),
+              NM_HASH_OBFUSCATE_PTR(reglist_data->l3cd));
+    }
+
+    if (!nm_l3cfg_remove_config(priv->config.l3cfg, L3CD_ACD_TAG(priv), reglist_data->l3cd))
+        nm_assert_not_reached();
+
+    nm_clear_l3cd(&reglist_data->l3cd);
+
+    nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_UPDATE);
+
+    g_array_remove_index(priv->v4.acd.reglist, idx);
+
+    if (priv->v4.acd.reglist->len == 0) {
+        nm_clear_pointer(&priv->v4.acd.reglist, g_array_unref);
+        nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source);
+    }
+}
+
+static gboolean
+_acd_reglist_timeout_cb(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gint64               now_msec;
+
+    nm_clear_g_source_inst(&priv->v4.acd.reglist_timeout_source);
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    while (nm_g_array_len(priv->v4.acd.reglist) > 0) {
+        AcdRegListData *reglist_data = _acd_reglist_data_get(priv, 0);
+
+        if (reglist_data->expiry_msec > now_msec)
+            break;
+
+        _acd_reglist_data_remove(self, 0, TRUE);
+    }
+
+    _acd_reglist_timeout_reschedule(self, now_msec);
+
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_acd_reglist_timeout_reschedule(NMDhcpClient *self, gint64 now_msec)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    AcdRegListData      *reglist_data;
+
+    if (nm_g_array_len(priv->v4.acd.reglist) == 0) {
+        nm_assert(!priv->v4.acd.reglist_timeout_source);
+        return;
+    }
+
+    if (priv->v4.acd.reglist_timeout_source) {
+        /* already pending. As we only add new elements with a *later*
+          * expiry, we don't need to ever cancel a pending timer. Worst
+          * case, the timer fires, and there is nothing to do and we
+          * reschedule. */
+        return;
+    }
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    reglist_data = _acd_reglist_data_get(priv, 0);
+
+    nm_assert(reglist_data->expiry_msec > now_msec);
+
+    priv->v4.acd.reglist_timeout_source =
+        nm_g_timeout_add_source(reglist_data->expiry_msec - now_msec,
+                                _acd_reglist_timeout_cb,
+                                self);
+}
+
+static void
+_acd_check_lease(NMDhcpClient *self, NMOptionBool *out_acd_state)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    char                 sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    in_addr_t            addr;
+    gboolean             addr_changed = FALSE;
+    guint                idx;
+    gint64               now_msec;
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        goto handle_no_acd;
+
+    if (!priv->l3cd_next)
+        goto handle_no_acd;
+
+    /* an IPv4 lease is always expected to have exactly one address. */
+    nm_assert(nm_l3_config_data_get_num_addresses(priv->l3cd_next, AF_INET) == 1);
+
+    if (priv->config.v4.acd_timeout_msec == 0)
+        goto handle_no_acd;
+
+    addr = NMP_OBJECT_CAST_IP4_ADDRESS(
+               nm_l3_config_data_get_first_obj(priv->l3cd_next, NMP_OBJECT_TYPE_IP4_ADDRESS, NULL))
+               ->address;
+    nm_assert(addr != INADDR_ANY);
+
+    nm_clear_g_source_inst(&priv->v4.acd.done_source);
+
+    if (priv->v4.acd.state != NM_OPTION_BOOL_DEFAULT && priv->v4.acd.addr == addr) {
+        /* the ACD state is already determined. Return right away. */
+        nm_assert(!priv->v4.acd.l3cfg_commit_handle);
+        *out_acd_state = !!priv->v4.acd.state;
+        return;
+    }
+
+    if (priv->v4.acd.addr != addr) {
+        addr_changed      = TRUE;
+        priv->v4.acd.addr = addr;
+    }
+
+    _LOGD("acd: %s check for address %s (timeout %u msec, l3cd " NM_HASH_OBFUSCATE_PTR_FMT ")",
+          addr_changed ? "add" : "update",
+          _nm_utils_inet4_ntop(addr, sbuf_addr),
+          priv->config.v4.acd_timeout_msec,
+          NM_HASH_OBFUSCATE_PTR(priv->l3cd_next));
+
+    priv->v4.acd.state = NM_OPTION_BOOL_DEFAULT;
+
+    if (nm_l3cfg_add_config(priv->config.l3cfg,
+                            L3CD_ACD_TAG(priv),
+                            FALSE,
+                            priv->l3cd_next,
+                            NM_L3CFG_CONFIG_PRIORITY_IPV4LL,
+                            0,
+                            0,
+                            NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP4,
+                            NM_PLATFORM_ROUTE_METRIC_DEFAULT_IP6,
+                            0,
+                            0,
+                            NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                            NM_DNS_PRIORITY_DEFAULT_NORMAL,
+                            NM_L3_ACD_DEFEND_TYPE_ONCE,
+                            NM_MIN(priv->config.v4.acd_timeout_msec, NM_ACD_TIMEOUT_MAX_MSEC),
+                            NM_L3CFG_CONFIG_FLAGS_ONLY_FOR_ACD,
+                            NM_L3_CONFIG_MERGE_FLAGS_NONE))
+        addr_changed = TRUE;
+
+    if (!priv->v4.acd.reglist)
+        priv->v4.acd.reglist = g_array_new(FALSE, FALSE, sizeof(AcdRegListData));
+
+    idx = _acd_reglist_data_find(priv, addr);
+
+    now_msec = nm_utils_get_monotonic_timestamp_msec();
+
+    g_array_append_val(priv->v4.acd.reglist,
+                       ((AcdRegListData){
+                           .l3cd        = nm_l3_config_data_ref(priv->l3cd_next),
+                           .addr        = addr,
+                           .expiry_msec = now_msec + ACD_REGLIST_GRACE_PERIOD_MSEC,
+                       }));
+
+    if (idx != G_MAXUINT) {
+        /* we already tracked this "addr". We don't need to track it twice,
+         * forget about this one. This also has the effect, that we will
+         * always append the new entry to the list (so the list
+         * stays sorted by the increasing timestamp). */
+        _acd_reglist_data_remove(self, idx, FALSE);
+    }
+
+    if (priv->v4.acd.reglist->len > ACD_REGLIST_MAX_ENTRIES) {
+        /* rate limit how many addresses we track for ACD. */
+        _acd_reglist_data_remove(self, 0, TRUE);
+    }
+
+    _acd_reglist_timeout_reschedule(self, now_msec);
+
+    if (!priv->v4.acd.l3cfg_commit_handle) {
+        priv->v4.acd.l3cfg_commit_handle =
+            nm_l3cfg_commit_type_register(priv->config.l3cfg,
+                                          NM_L3_CFG_COMMIT_TYPE_UPDATE,
+                                          NULL,
+                                          "dhcp4-acd");
+        l3_cfg_notify_check_connected(self);
+    }
+
+    if (addr_changed)
+        nm_l3cfg_commit_on_idle_schedule(priv->config.l3cfg, NM_L3_CFG_COMMIT_TYPE_AUTO);
+
+    /* ACD is started/pending... */
+    nm_assert(priv->v4.acd.addr != INADDR_ANY);
+    nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+    nm_assert(priv->v4.acd.l3cfg_commit_handle);
+    nm_assert(priv->l3cfg_notify.id);
+    *out_acd_state = NM_OPTION_BOOL_DEFAULT;
+    return;
+
+handle_no_acd:
+    /* Indicate that ACD is good (or disabled) by returning TRUE. */
+    _acd_state_reset(self, TRUE, FALSE);
+    *out_acd_state = NM_OPTION_BOOL_TRUE;
+    return;
+}
+
+/*****************************************************************************/
+
+gboolean
+_nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr)
+{
+    NMDhcpClientPrivate   *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    char                   sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
+    NMIPAddr               yiaddr;
+    const NML3AcdAddrInfo *acd_info;
+
+    if (!NM_IS_IPv4(priv->config.addr_family))
+        return nm_assert_unreachable_val(FALSE);
+
+    if (priv->config.v4.acd_timeout_msec == 0) {
+        /* ACD is disabled. Note that we might track the address for other
+         * reasons and have information about the ACD state below. But
+         * with ACD disabled, we always ignore that information. */
+        return TRUE;
+    }
+
+    nm_ip_addr_set(priv->config.addr_family, &yiaddr, p_yiaddr);
+
+    /* Note that once we do ACD for a certain address, even after completing
+     * it, we keep the l3cd registered in NML3Cfg for ACD_REGLIST_GRACE_PERIOD_MSEC
+     * The idea is, that we don't yet turn off ACD for a grace period, so that
+     * we can avoid selecting the same lease again.
+     *
+     * Note that we even check whether we have an ACD state if priv->v4.acd.reglist
+     * is empty. Maybe for odd reasons, we track ACD for the address already. */
+
+    acd_info = nm_l3cfg_get_acd_addr_info(priv->config.l3cfg, yiaddr.addr4);
+
+    if (!acd_info)
+        return TRUE;
+
+    if (!NM_IN_SET(acd_info->state, NM_L3_ACD_ADDR_STATE_USED, NM_L3_ACD_ADDR_STATE_CONFLICT))
+        return TRUE;
+
+    _LOGD("offered lease rejected: address %s failed ACD check",
+          _nm_utils_inet4_ntop(yiaddr.addr4, sbuf_addr));
+
+    return FALSE;
+}
+
 void
 _nm_dhcp_client_notify(NMDhcpClient         *self,
                        NMDhcpClientEventType client_event_type,
@@ -313,6 +722,8 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
 {
     NMDhcpClientPrivate                     *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     GHashTable                              *options;
+    gboolean                                 l3cd_changed;
+    NMOptionBool                             acd_state;
     const int                                IS_IPv4     = NM_IS_IPv4(priv->config.addr_family);
     nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL;
     char                                     sbuf1[NM_HASH_OBFUSCATE_PTR_STR_BUF_SIZE];
@@ -343,8 +754,7 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
           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);
+    nm_l3_config_data_seal(l3cd);
 
     if (client_event_type >= NM_DHCP_CLIENT_EVENT_TYPE_TIMEOUT)
         watch_cleanup(self);
@@ -353,33 +763,40 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
         /* 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)) {
+        if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd_next, l3cd, &l3cd_merged)) {
+            _LOGD("lease merged with existing one");
             l3cd = nm_l3_config_data_seal(l3cd_merged);
         }
     }
 
-    if (priv->l3cd == l3cd)
-        return;
-
     if (l3cd) {
         nm_clear_g_source_inst(&priv->no_lease_timeout_source);
-    } else {
-        if (priv->l3cd)
-            _no_lease_timeout_schedule(self);
-    }
+    } else
+        _no_lease_timeout_schedule(self);
 
-    /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API.
-     * The internals like the state should not be exposed (or possibly dropped in large
-     * parts). */
+    l3cd_changed = nm_l3_config_data_reset(&priv->l3cd_next, l3cd);
 
-    nm_l3_config_data_reset(&priv->l3cd, l3cd);
+    _acd_check_lease(self, &acd_state);
 
-    options = l3cd ? nm_dhcp_lease_get_options(
-                  nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family))
-                   : NULL;
+    options = priv->l3cd_next ? nm_dhcp_lease_get_options(
+                  nm_l3_config_data_get_dhcp_lease(priv->l3cd_next, priv->config.addr_family))
+                              : NULL;
+
+    if (_LOGI_ENABLED()) {
+        const char *req_str =
+            IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS)
+                    : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS);
+        const char *addr = nm_g_hash_table_lookup(options, req_str);
+
+        _LOGI("state changed %s%s%s%s",
+              priv->l3cd_next ? "new lease" : "no lease",
+              NM_PRINT_FMT_QUOTED2(addr, ", address=", addr, ""),
+              acd_state == NM_OPTION_BOOL_DEFAULT ? ", acd pending"
+                                                  : (acd_state ? "" : ", acd conflict"));
+    }
 
     if (_LOGD_ENABLED()) {
-        if (options) {
+        if (l3cd_changed && options) {
             gs_free const char **keys = NULL;
             guint                nkeys;
             guint                i;
@@ -390,58 +807,41 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
                       keys[i],
                       (char *) g_hash_table_lookup(options, keys[i]));
             }
-
-            if (priv->config.addr_family == AF_INET6) {
-                gs_free char *event_id = NULL;
-
-                event_id = nm_dhcp_utils_get_dhcp6_event_id(options);
-                if (event_id)
-                    _LOGT("event-id: \"%s\"", event_id);
-            }
         }
     }
 
-    if (_LOGI_ENABLED()) {
-        const char *req_str =
-            IS_IPv4 ? nm_dhcp_option_request_string(AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS)
-                    : nm_dhcp_option_request_string(AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS);
-        const char *addr = nm_g_hash_table_lookup(options, req_str);
+    if (acd_state == NM_OPTION_BOOL_DEFAULT) {
+        /* ACD is in progress... */
+        return;
+    }
 
-        _LOGI("state changed %s%s%s%s",
-              priv->l3cd ? "new lease" : "no lease",
-              NM_PRINT_FMT_QUOTED(addr, ", address=", addr, "", ""));
+    if (!acd_state) {
+        gs_free_error GError *error = NULL;
+
+        /* We only decline. We don't actually emit to the caller that
+         * something is wrong (like NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD).
+         * If we would, NMDevice might decide to tear down the device, when
+         * we actually should continue trying to get a better lease. There
+         * is already "ipv4.dhcp-timeout" which will handle the failure if
+         * we don't get a good lease. */
+        if (!_dhcp_client_decline(self, priv->l3cd_next, "acd failed", &error))
+            _LOGD("decline failed: %s", error->message);
+        return;
     }
 
-    /* FIXME(l3cfg:dhcp:acd): NMDhcpClient must also do ACD. It needs acd_timeout_msec
-     * 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 _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),
-     * and if the address appears there, we need to accept the lease. That is complicated
-     * but necessary, because we can only accept the lease after we configured the
-     * address.
-     *
-     * As a whole, ACD is transparent for the user (NMDevice). It's entirely managed
-     * by NMDhcpClient. Note that we do ACD through NML3Cfg, which centralizes IP handling
-     * for one interface, so for example if the same address happens to be configured
-     * as a static address (bypassing ACD), then NML3Cfg is aware of that and signals
-     * immediate success. */
-
-    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) {
+    nm_l3_config_data_reset(&priv->l3cd_curr, priv->l3cd_next);
+
+    if (client_event_type == NM_DHCP_CLIENT_EVENT_TYPE_BOUND && priv->l3cd_curr
+        && nm_l3_config_data_get_num_addresses(priv->l3cd_curr, priv->config.addr_family) > 0)
         priv->l3cfg_notify.wait_dhcp_commit = TRUE;
-    } else {
+    else
         priv->l3cfg_notify.wait_dhcp_commit = FALSE;
-    }
 
-    if (!priv->l3cfg_notify.wait_dhcp_commit && priv->l3cd) {
+    if (!priv->l3cfg_notify.wait_dhcp_commit && priv->l3cd_curr) {
         gs_free_error GError *error = NULL;
 
         _LOGD("accept lease right away");
-        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
+        if (!_dhcp_client_accept(self, priv->l3cd_curr, &error)) {
             _LOGD("accept failed: %s", error->message);
             /* Unclear why this happened, or what to do about it. Just proceed. */
         }
@@ -454,7 +854,7 @@ _nm_dhcp_client_notify(NMDhcpClient         *self,
             .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
             .lease_update =
                 {
-                    .l3cd     = priv->l3cd,
+                    .l3cd     = priv->l3cd_curr,
                     .accepted = !priv->l3cfg_notify.wait_dhcp_commit,
                 },
         };
@@ -529,7 +929,7 @@ _dhcp_client_accept(NMDhcpClient *self, const NML3ConfigData *l3cd, GError **err
 
     klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_curr, FALSE);
 
     return klass->accept(self, l3cd, error);
 }
@@ -569,7 +969,7 @@ _dhcp_client_decline(NMDhcpClient         *self,
 
     klass = NM_DHCP_CLIENT_GET_CLASS(self);
 
-    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd, FALSE);
+    g_return_val_if_fail(NM_DHCP_CLIENT_GET_PRIVATE(self)->l3cd_next, FALSE);
 
     return klass->decline(self, l3cd, error_message, error);
 }
@@ -626,7 +1026,9 @@ ipv6_lladdr_find(NMDhcpClient *self)
     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));
+    nmp_lookup_init_object_by_ifindex(&lookup,
+                                      NMP_OBJECT_TYPE_IP6_ADDRESS,
+                                      nm_l3cfg_get_ifindex(l3cfg));
 
     nm_platform_iter_obj_for_each (&iter, nm_l3cfg_get_platform(l3cfg), &lookup, &obj) {
         const NMPlatformIP6Address *pladdr = NMP_OBJECT_CAST_IP6_ADDRESS(obj);
@@ -651,7 +1053,7 @@ ipv6_tentative_addr_find(NMDhcpClient *self)
 
     /* 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) {
+    nm_l3_config_data_iter_ip6_address_for_each (&iter, priv->l3cd_curr, &addr) {
         const NMPlatformIP6Address *pladdr;
         NMPObject                   needle;
 
@@ -676,6 +1078,7 @@ static void
 l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    char                 sbuf_addr[NM_UTILS_INET_ADDRSTRLEN];
 
     nm_assert(l3cfg == priv->config.l3cfg);
 
@@ -717,7 +1120,7 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                 self,
                 &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
                                            .lease_update = {
-                                               .l3cd     = priv->l3cd,
+                                               .l3cd     = priv->l3cd_curr,
                                                .accepted = TRUE,
                                            }}));
         }
@@ -736,7 +1139,7 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
          * lease and notifying NMDevice. */
 
         nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter,
-                                                    priv->l3cd,
+                                                    priv->l3cd_curr,
                                                     priv->config.addr_family,
                                                     &lease_address)
             break;
@@ -778,9 +1181,11 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
 
         _LOGD("accept lease");
 
-        if (!_dhcp_client_accept(self, priv->l3cd, &error)) {
+        if (!_dhcp_client_accept(self, priv->l3cd_curr, &error)) {
             gs_free char *reason = g_strdup_printf("error accepting lease: %s", error->message);
 
+            _LOGD("accept failed: %s", error->message);
+
             _emit_notify(self,
                          &((NMDhcpClientNotifyData){
                              .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
@@ -794,12 +1199,54 @@ l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcp
                 self,
                 &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
                                            .lease_update = {
-                                               .l3cd     = priv->l3cd,
+                                               .l3cd     = priv->l3cd_curr,
                                                .accepted = TRUE,
                                            }}));
         }
     }
-wait_dhcp_commit_done:;
+wait_dhcp_commit_done:
+
+    if (notify_data->notify_type == NM_L3_CONFIG_NOTIFY_TYPE_ACD_EVENT
+        && priv->v4.acd.l3cfg_commit_handle) {
+        nm_assert(priv->v4.acd.addr != INADDR_ANY);
+        nm_assert(priv->v4.acd.state == NM_OPTION_BOOL_DEFAULT);
+        nm_assert(!priv->v4.acd.done_source);
+
+        if (priv->v4.acd.addr == notify_data->acd_event.info.addr
+            && nm_l3_acd_addr_info_find_track_info(&notify_data->acd_event.info,
+                                                   L3CD_ACD_TAG(priv),
+                                                   NULL,
+                                                   NULL)) {
+            NMOptionBool acd_state;
+
+            switch (notify_data->acd_event.info.state) {
+            default:
+                nm_assert_not_reached();
+                /* fall-through */
+            case NM_L3_ACD_ADDR_STATE_INIT:
+            case NM_L3_ACD_ADDR_STATE_PROBING:
+                acd_state = NM_OPTION_BOOL_DEFAULT;
+                break;
+            case NM_L3_ACD_ADDR_STATE_USED:
+            case NM_L3_ACD_ADDR_STATE_CONFLICT:
+            case NM_L3_ACD_ADDR_STATE_EXTERNAL_REMOVED:
+                acd_state = NM_OPTION_BOOL_FALSE;
+                break;
+            case NM_L3_ACD_ADDR_STATE_READY:
+            case NM_L3_ACD_ADDR_STATE_DEFENDING:
+                acd_state = NM_OPTION_BOOL_TRUE;
+                break;
+            }
+            if (acd_state != NM_OPTION_BOOL_DEFAULT) {
+                _LOGD("acd: acd %s for %s",
+                      acd_state ? "ready" : "conflict",
+                      _nm_utils_inet4_ntop(priv->v4.acd.addr, sbuf_addr));
+                nm_l3cfg_commit_type_clear(priv->config.l3cfg, &priv->v4.acd.l3cfg_commit_handle);
+                priv->v4.acd.state       = acd_state;
+                priv->v4.acd.done_source = nm_g_idle_add_source(_acd_complete_on_idle_cb, self);
+            }
+        }
+    }
 }
 
 gboolean
@@ -928,6 +1375,8 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
                                               "dhcp stopping");
     }
 
+    _acd_state_reset(self, TRUE, TRUE);
+
     priv->l3cfg_notify.wait_dhcp_commit = FALSE;
     priv->l3cfg_notify.wait_ll_address  = FALSE;
     priv->l3cfg_notify.wait_ipv6_dad    = FALSE;
@@ -942,6 +1391,9 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
         _LOGI("canceled DHCP transaction");
     nm_assert(priv->pid == -1);
 
+    nm_clear_l3cd(&priv->l3cd_next);
+    nm_clear_l3cd(&priv->l3cd_curr);
+
     _nm_dhcp_client_notify(self, NM_DHCP_CLIENT_EVENT_TYPE_TERMINATED, NULL);
 }
 
@@ -951,37 +1403,31 @@ static char *
 bytearray_variant_to_string(NMDhcpClient *self, GVariant *value, const char *key)
 {
     const guint8 *array;
+    char         *str;
     gsize         length;
-    GString      *str;
-    int           i;
-    unsigned char c;
-    char         *converted = NULL;
+    gsize         i;
 
-    g_return_val_if_fail(value != NULL, NULL);
+    nm_assert(value);
 
     array = g_variant_get_fixed_array(value, &length, 1);
 
-    /* Since the DHCP options come through environment variables, they should
-     * already be UTF-8 safe, but just make sure.
+    /* Since the DHCP options come originally came as environment variables, they
+     * have not guaranteed encoding. Let's only accept ASCII here.
      */
-    str = g_string_sized_new(length);
+    str = g_malloc(length + 1);
     for (i = 0; i < length; i++) {
-        c = array[i];
+        guint8 c = array[i];
 
-        /* Convert NULLs to spaces and non-ASCII characters to ? */
         if (c == '\0')
-            c = ' ';
+            str[i] = ' ';
         else if (c > 127)
-            c = '?';
-        str = g_string_append_c(str, c);
+            str[i] = '?';
+        else
+            str[i] = (char) c;
     }
-    str = g_string_append_c(str, '\0');
+    str[i] = '\0';
 
-    converted = str->str;
-    if (!g_utf8_validate(converted, -1, NULL))
-        _LOGW("option '%s' couldn't be converted to UTF-8", key);
-    g_string_free(str, FALSE);
-    return converted;
+    return str;
 }
 
 static int
@@ -1004,11 +1450,13 @@ label_is_unknown_xyz(const char *label)
 static void
 maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant *value)
 {
-    char *str_value = NULL;
+    char *str_value;
+    int   priv_opt_num;
 
-    g_return_if_fail(g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING));
+    if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING))
+        return;
 
-    if (g_str_has_prefix(key, OLD_TAG))
+    if (NM_STR_HAS_PREFIX(key, OLD_TAG))
         return;
 
     /* Filter out stuff that's not actually new DHCP options */
@@ -1021,34 +1469,33 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
         return;
 
     str_value = bytearray_variant_to_string(self, value, key);
-    if (str_value) {
-        int priv_opt_num;
+    if (!str_value)
+        return;
 
-        g_hash_table_insert(hash, g_strdup(key), str_value);
+    g_hash_table_insert(hash, g_strdup(key), str_value);
 
-        /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
+    /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
          * labels for that. We need to identify those to alias them to our "private_xyz"
          * format unused in the internal dchp plugins.
          */
-        if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
-            gs_free guint8 *check_val = NULL;
-            char           *hex_str   = NULL;
-            gsize           len;
+    if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
+        gs_free guint8 *check_val = NULL;
+        char           *hex_str   = NULL;
+        gsize           len;
 
-            /* dhclient passes values from dhcp private options in its own "string" format:
+        /* dhclient passes values from dhcp private options in its own "string" format:
              * if the raw values are printable as ascii strings, it will pass the string
              * representation; if the values are not printable as an ascii string, it will
              * pass a string displaying the hex values (hex string). Try to enforce passing
              * always an hex string, converting string representation if needed.
              */
-            check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
-            hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
-                                               check_val ? len : strlen(str_value),
-                                               ':',
-                                               FALSE,
-                                               NULL);
-            g_hash_table_insert(hash, g_strdup_printf("private_%d", priv_opt_num), hex_str);
-        }
+        check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
+        hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
+                                           check_val ? len : strlen(str_value),
+                                           ':',
+                                           FALSE,
+                                           NULL);
+        g_hash_table_insert(hash, g_strdup_printf("private_%d", priv_opt_num), hex_str);
     }
 }
 
@@ -1079,8 +1526,9 @@ nm_dhcp_client_handle_event(gpointer               unused,
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
     NMDhcpClientEventType                   client_event_type;
     NMPlatformIP6Address                    prefix = {
-        0,
+                           0,
     };
+    int IS_IPv4;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
     g_return_val_if_fail(iface != NULL, FALSE);
@@ -1091,6 +1539,8 @@ nm_dhcp_client_handle_event(gpointer               unused,
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
+    g_return_val_if_fail(!priv->is_stopped, FALSE);
+
     if (!nm_streq0(priv->config.iface, iface))
         return FALSE;
     if (priv->pid != pid)
@@ -1175,10 +1625,12 @@ nm_dhcp_client_handle_event(gpointer               unused,
         client_event_type = NM_DHCP_CLIENT_EVENT_TYPE_FAIL;
     }
 
-    if (priv->v4.bound.invocation)
+    IS_IPv4 = NM_IS_IPv4(priv->config.addr_family);
+
+    if (IS_IPv4 && 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)
+    if (IS_IPv4
         && NM_IN_SET(client_event_type,
                      NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
                      NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED))
@@ -1202,23 +1654,23 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
     /* IPv6 not implemented yet */
     nm_assert(priv->config.addr_family == AF_INET);
 
-    if (!priv->config.reject_servers || !priv->config.reject_servers[0])
-        return FALSE;
-
-    for (i = 0; priv->config.reject_servers[i]; i++) {
-        in_addr_t r_addr;
-        in_addr_t mask;
-        int       r_prefix;
-
-        if (!nm_utils_parse_inaddr_prefix_bin(AF_INET,
-                                              priv->config.reject_servers[i],
-                                              NULL,
-                                              &r_addr,
-                                              &r_prefix))
-            nm_assert_not_reached();
-        mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix);
-        if ((addr4 & mask) == (r_addr & mask))
-            return TRUE;
+    if (priv->config.reject_servers) {
+        for (i = 0; priv->config.reject_servers[i]; i++) {
+            in_addr_t r_addr;
+            in_addr_t mask;
+            int       r_prefix;
+
+            if (!nm_utils_parse_inaddr_prefix_bin(AF_INET,
+                                                  priv->config.reject_servers[i],
+                                                  NULL,
+                                                  &r_addr,
+                                                  &r_prefix))
+                nm_assert_not_reached();
+
+            mask = _nm_utils_ip4_prefix_to_netmask(r_prefix < 0 ? 32 : r_prefix);
+            if ((addr4 & mask) == (r_addr & mask))
+                return TRUE;
+        }
     }
 
     return FALSE;
@@ -1251,7 +1703,7 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
     config->hostname        = g_strdup(config->hostname);
     config->mud_url         = g_strdup(config->mud_url);
 
-    config->reject_servers = (const char *const *) nm_strv_dup(config->reject_servers, -1, TRUE);
+    config->reject_servers = nm_strv_dup_packed(config->reject_servers, -1);
 
     if (NM_IS_IPv4(config->addr_family))
         config->v4.last_address = g_strdup(config->v4.last_address);
@@ -1284,7 +1736,7 @@ config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
         if (!config->send_hostname) {
             nm_clear_g_free((gpointer *) &config->hostname);
         } else if ((config->use_fqdn && !nm_sd_dns_name_is_valid(config->hostname))
-                   || (!config->use_fqdn && !nm_sd_hostname_is_valid(config->hostname, FALSE))) {
+                   || (!config->use_fqdn && !nm_hostname_is_valid(config->hostname, FALSE))) {
             nm_log_warn(LOGD_DHCP,
                         "dhcp%c: %s '%s' is invalid, will be ignored",
                         nm_utils_addr_family_to_char(config->addr_family),
@@ -1310,8 +1762,7 @@ config_clear(NMDhcpClientConfig *config)
     nm_clear_g_free((gpointer *) &config->anycast_address);
     nm_clear_g_free((gpointer *) &config->hostname);
     nm_clear_g_free((gpointer *) &config->mud_url);
-
-    nm_clear_pointer((gpointer *) &config->reject_servers, g_strfreev);
+    nm_clear_g_free((gpointer *) &config->reject_servers);
 
     if (config->addr_family == AF_INET) {
         nm_clear_g_free((gpointer *) &config->v4.last_address);
@@ -1338,6 +1789,13 @@ set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *ps
                     {
                         .invocation = NULL,
                     },
+                .acd =
+                    {
+                        .addr                = INADDR_ANY,
+                        .state               = NM_OPTION_BOOL_DEFAULT,
+                        .l3cfg_commit_handle = NULL,
+                        .done_source         = NULL,
+                    },
             };
         } else {
             priv->v6 = (typeof(priv->v6)){
@@ -1383,6 +1841,11 @@ dispose(GObject *object)
 
     nm_clear_pointer(&priv->effective_client_id, g_bytes_unref);
 
+    nm_assert(!priv->watch_source);
+    nm_assert(!priv->l3cd_next);
+    nm_assert(!priv->l3cd_curr);
+    nm_assert(priv->l3cfg_notify.id == 0);
+
     G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object);
 }
 
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index e4b99929..51c6bc04 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -150,6 +150,10 @@ typedef struct {
             /* The address from the previous lease */
             const char *last_address;
 
+            /* Whether to do ACD for the DHCPv4 address. With timeout zero, ACD
+             * is disabled. */
+            guint acd_timeout_msec;
+
             /* Set BOOTP broadcast flag in request packets, so that servers
              * will always broadcast replies. */
             bool request_broadcast : 1;
@@ -261,6 +265,8 @@ void _nm_dhcp_client_notify(NMDhcpClient         *self,
                             NMDhcpClientEventType client_event_type,
                             const NML3ConfigData *l3cd);
 
+gboolean _nm_dhcp_client_accept_offer(NMDhcpClient *self, gconstpointer p_yiaddr);
+
 gboolean nm_dhcp_client_handle_event(gpointer               unused,
                                      const char            *iface,
                                      int                    pid,
@@ -282,6 +288,8 @@ int                nm_dhcp_client_get_ifindex(NMDhcpClient *self);
 void    nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id);
 GBytes *nm_dhcp_client_get_effective_client_id(NMDhcpClient *self);
 
+NML3ConfigData *nm_dhcp_client_create_l3cd(NMDhcpClient *self);
+
 /*****************************************************************************
  * Client data
  *****************************************************************************/
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index aab658a2..5a17f4e8 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -103,14 +103,17 @@ next:;
 int
 main(int argc, char *argv[])
 {
-    gs_unref_object GDBusConnection *connection = NULL;
-    gs_free_error GError            *error      = NULL;
-    gs_unref_variant GVariant       *parameters = NULL;
-    gs_unref_variant GVariant       *result     = NULL;
-    gboolean                         success    = FALSE;
+    gs_unref_object GDBusConnection *connection  = NULL;
+    gs_free_error GError            *error       = NULL;
+    gs_free_error GError            *error_flush = NULL;
+    gs_unref_variant GVariant       *parameters  = NULL;
+    gs_unref_variant GVariant       *result      = NULL;
+    gs_free char                    *s_err       = NULL;
+    gboolean                         success;
     guint                            try_count;
     gint64                           time_start;
     gint64                           time_end;
+    gint64                           remaining_time;
 
     /* Connecting to the unix socket can fail with EAGAIN if there are too
      * many pending connections and the server can't accept them in time
@@ -121,6 +124,8 @@ main(int argc, char *argv[])
     time_end   = time_start + (5000 * 1000L);
     try_count  = 0;
 
+    _LOGi("nm-dhcp-helper: event called");
+
 do_connect:
     try_count++;
     connection =
@@ -131,16 +136,16 @@ do_connect:
                                                &error);
     if (!connection) {
         if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
-            gint64 time_remaining = time_end - g_get_monotonic_time();
-            gint64 interval;
+            remaining_time = time_end - g_get_monotonic_time();
+            if (remaining_time > 0) {
+                gint64 interval;
 
-            if (time_remaining > 0) {
                 _LOGi("failure to connect: %s (retry %u, waited %lld ms)",
                       error->message,
                       try_count,
-                      (long long) (time_end - time_remaining - time_start) / 1000);
+                      (long long) (time_end - remaining_time - time_start) / 1000);
                 interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 100000);
-                g_usleep(NM_MIN(interval, time_remaining));
+                g_usleep(NM_MIN(interval, remaining_time));
                 g_clear_error(&error);
                 goto do_connect;
             }
@@ -148,6 +153,7 @@ do_connect:
 
         g_dbus_error_strip_remote_error(error);
         _LOGE("could not connect to NetworkManager D-Bus socket: %s", error->message);
+        success = FALSE;
         goto out;
     }
 
@@ -169,57 +175,74 @@ do_notify:
                                          NULL,
                                          &error);
 
-    if (!result) {
-        gs_free char *s_err = NULL;
+    if (result) {
+        success = TRUE;
+        goto out;
+    }
 
-        s_err = g_dbus_error_get_remote_error(error);
-        if (NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) {
-            gint64 remaining_time = time_end - g_get_monotonic_time();
-            gint64 interval;
+    s_err = g_dbus_error_get_remote_error(error);
 
-            /* I am not sure that a race can actually happen, as we register the object
-             * on the server side during GDBusServer:new-connection signal.
-             *
-             * However, there was also a race for subscribing to an event, so let's just
-             * do some retry. */
-            if (remaining_time > 0) {
-                _LOGi("failure to call notify: %s (retry %u)", error->message, try_count);
-                interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000);
-                g_usleep(NM_MIN(interval, remaining_time));
-                g_clear_error(&error);
-                goto do_notify;
-            }
-        }
+    if (NM_IN_STRSET(s_err, "org.freedesktop.NetworkManager.Device.Failed")) {
+        _LOGi("notify failed with reason: %s", error->message);
+        success = FALSE;
+        goto out;
+    }
+
+    if (!NM_IN_STRSET(s_err, "org.freedesktop.DBus.Error.UnknownMethod")) {
+        /* Some unexpected error. We treat that as a failure. In particular,
+         * the daemon will fail the request if ACD fails. This causes nm-dhcp-helper
+         * to fail, which in turn causes dhclient to send a DECLINE. */
         _LOGW("failure to call notify: %s (try signal via Event)", error->message);
+        success = FALSE;
+        goto out;
+    }
+
+    /* I am not sure that a race can actually happen, as we register the object
+     * on the server side during GDBusServer:new-connection signal.
+     *
+     * However, there was also a race for subscribing to an event, so let's just
+     * do some retry. */
+    remaining_time = time_end - g_get_monotonic_time();
+    if (remaining_time > 0) {
+        gint64 interval;
+
+        _LOGi("failure to call notify: %s (retry %u)", error->message, try_count);
+        interval = NM_CLAMP((gint64) (100L * (1L << NM_MIN(try_count, 31))), 5000, 25000);
+        g_usleep(NM_MIN(interval, remaining_time));
         g_clear_error(&error);
+        goto do_notify;
+    }
 
-        /* for backward compatibility, try to emit the signal. There is no stable
-         * API between the dhcp-helper and NetworkManager. However, while upgrading
-         * the NetworkManager package, a newer helper might want to notify an
-         * older server, which still uses the "Event". */
-        if (!g_dbus_connection_emit_signal(connection,
-                                           NULL,
-                                           "/",
-                                           NM_DHCP_CLIENT_DBUS_IFACE,
-                                           "Event",
-                                           parameters,
-                                           &error)) {
-            g_dbus_error_strip_remote_error(error);
-            _LOGE("could not send DHCP Event signal: %s", error->message);
-            goto out;
-        }
+    /* for backward compatibility, try to emit the signal. There is no stable
+     * API between the dhcp-helper and NetworkManager. However, while upgrading
+     * the NetworkManager package, a newer helper might want to notify an
+     * older server, which still uses the "Event". */
+
+    _LOGW("failure to call notify: %s (try signal via Event)", error->message);
+    g_clear_error(&error);
+
+    if (g_dbus_connection_emit_signal(connection,
+                                      NULL,
+                                      "/",
+                                      NM_DHCP_CLIENT_DBUS_IFACE,
+                                      "Event",
+                                      parameters,
+                                      &error)) {
         /* We were able to send the asynchronous Event. Consider that a success. */
         success = TRUE;
-    } else
-        success = TRUE;
-
-    if (!g_dbus_connection_flush_sync(connection, NULL, &error)) {
-        g_dbus_error_strip_remote_error(error);
-        _LOGE("could not flush D-Bus connection: %s", error->message);
-        success = FALSE;
         goto out;
     }
 
+    g_dbus_error_strip_remote_error(error);
+    _LOGE("could not send DHCP Event signal: %s", error->message);
+    success = FALSE;
+
 out:
+    if (!g_dbus_connection_flush_sync(connection, NULL, &error_flush)) {
+        _LOGE("could not flush D-Bus connection: %s", error_flush->message);
+        /* if we considered this a success so far, don't fail because of this. */
+    }
+
+    _LOGi("success: %s", success ? "YES" : "NO");
     return success ? EXIT_SUCCESS : EXIT_FAILURE;
 }
diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c
index 9fea1666..cfff23f8 100644
--- a/src/core/dhcp/nm-dhcp-manager.c
+++ b/src/core/dhcp/nm-dhcp-manager.c
@@ -42,6 +42,30 @@ G_DEFINE_TYPE(NMDhcpManager, nm_dhcp_manager, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+#undef _NMLOG_ENABLED
+#define _NMLOG_ENABLED(level, addr_family) nm_logging_enabled((level), _LOGD_DHCP(addr_family))
+
+#define _NMLOG(level, addr_family, ...)                                                           \
+    G_STMT_START                                                                                  \
+    {                                                                                             \
+        const int         _addr_family = (addr_family);                                           \
+        const NMLogLevel  _log_level   = (level);                                                 \
+        const NMLogDomain _log_domain  = LOGD_DHCP_af(_addr_family);                              \
+                                                                                                  \
+        if (nm_logging_enabled(_log_level, _log_domain)) {                                        \
+            _nm_log(_log_level,                                                                   \
+                    _log_domain,                                                                  \
+                    0,                                                                            \
+                    NULL,                                                                         \
+                    NULL,                                                                         \
+                    "dhcp%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
+                    nm_utils_addr_family_to_str(_addr_family) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+        }                                                                                         \
+    }                                                                                             \
+    G_STMT_END
+
+/*****************************************************************************/
+
 /* default to installed helper, but can be modified for testing */
 const char *nm_dhcp_helper_path = LIBEXECDIR "/nm-dhcp-helper";
 
@@ -167,11 +191,10 @@ nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GE
 
     gtype = _client_factory_get_gtype(priv->client_factory, config->addr_family);
 
-    nm_log_trace(LOGD_DHCP,
-                 "dhcp%c: creating IPv%c DHCP client of type %s",
-                 nm_utils_addr_family_to_char(config->addr_family),
-                 nm_utils_addr_family_to_char(config->addr_family),
-                 g_type_name(gtype));
+    _LOGT(config->addr_family,
+          "creating IPv%c DHCP client of type %s",
+          nm_utils_addr_family_to_char(config->addr_family),
+          g_type_name(gtype));
 
     client = g_object_new(gtype, NM_DHCP_CLIENT_CONFIG, config, NULL);
 
@@ -244,11 +267,11 @@ nm_dhcp_manager_init(NMDhcpManager *self)
         if (!f)
             continue;
 
-        nm_log_dbg(LOGD_DHCP,
-                   "dhcp-init: enabled DHCP client '%s'%s%s",
-                   f->name,
-                   _client_factory_available(f) ? "" : " (not available)",
-                   f->undocumented ? " (undocumented internal plugin)" : "");
+        _LOGD(AF_UNSPEC,
+              "init: enabled DHCP client '%s'%s%s",
+              f->name,
+              _client_factory_available(f) ? "" : " (not available)",
+              f->undocumented ? " (undocumented internal plugin)" : "");
     }
 
     /* Client-specific setup */
@@ -261,20 +284,20 @@ nm_dhcp_manager_init(NMDhcpManager *self)
     if (client) {
         client_factory = _client_factory_available(_client_factory_find_by_name(client));
         if (!client_factory)
-            nm_log_warn(LOGD_DHCP, "dhcp-init: DHCP client '%s' not available", client);
+            _LOGW(AF_UNSPEC, "init: DHCP client '%s' not available", client);
     }
     if (!client_factory) {
         client_factory = _client_factory_find_by_name("" NM_CONFIG_DEFAULT_MAIN_DHCP);
         if (!client_factory)
-            nm_log_err(LOGD_DHCP,
-                       "dhcp-init: default DHCP client '%s' is not installed",
-                       NM_CONFIG_DEFAULT_MAIN_DHCP);
+            _LOGE(AF_UNSPEC,
+                  "init: default DHCP client '%s' is not installed",
+                  NM_CONFIG_DEFAULT_MAIN_DHCP);
         else {
             client_factory = _client_factory_available(client_factory);
             if (!client_factory)
-                nm_log_info(LOGD_DHCP,
-                            "dhcp-init: default DHCP client '%s' is not available",
-                            NM_CONFIG_DEFAULT_MAIN_DHCP);
+                _LOGI(AF_UNSPEC,
+                      "init: default DHCP client '%s' is not available",
+                      NM_CONFIG_DEFAULT_MAIN_DHCP);
         }
     }
     if (!client_factory) {
@@ -287,7 +310,7 @@ nm_dhcp_manager_init(NMDhcpManager *self)
 
     g_return_if_fail(client_factory);
 
-    nm_log_info(LOGD_DHCP, "dhcp-init: Using DHCP client '%s'", client_factory->name);
+    _LOGI(AF_UNSPEC, "init: Using DHCP client '%s'", client_factory->name);
 
     /* NOTE: currently the DHCP plugin is chosen once at start. It's not
      * possible to reload that configuration. If that ever becomes possible,
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index 2e41cf15..05b7b52e 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -13,22 +13,24 @@
 #include <ctype.h>
 #include <net/if_arp.h>
 
+#include "n-dhcp4/src/n-dhcp4.h"
+
 #include "libnm-glib-aux/nm-dedup-multi.h"
-#include "libnm-std-aux/unaligned.h"
+#include "libnm-glib-aux/nm-io-utils.h"
 #include "libnm-glib-aux/nm-str-buf.h"
+#include "libnm-std-aux/unaligned.h"
 
-#include "nm-l3-config-data.h"
-#include "nm-utils.h"
-#include "nm-config.h"
-#include "nm-dhcp-utils.h"
-#include "nm-dhcp-options.h"
-#include "nm-core-utils.h"
 #include "NetworkManagerUtils.h"
 #include "libnm-platform/nm-platform.h"
+#include "nm-config.h"
+#include "nm-core-utils.h"
 #include "nm-dhcp-client-logging.h"
-#include "n-dhcp4/src/n-dhcp4.h"
+#include "nm-dhcp-options.h"
+#include "nm-dhcp-utils.h"
+#include "nm-l3-config-data.h"
+#include "nm-utils.h"
+
 #include "libnm-systemd-shared/nm-sd-utils-shared.h"
-#include "libnm-systemd-core/nm-sd-utils-dhcp.h"
 
 /*****************************************************************************/
 
@@ -56,6 +58,8 @@ typedef struct {
         const NML3ConfigData *lease_l3cd;
     } granted;
 
+    GSource *pop_all_events_on_idle_source;
+
     GSource *event_source;
     char    *lease_file;
 } NMDhcpNettoolsPrivate;
@@ -76,6 +80,10 @@ G_DEFINE_TYPE(NMDhcpNettools, nm_dhcp_nettools, NM_TYPE_DHCP_CLIENT)
 
 /*****************************************************************************/
 
+static void dhcp4_event_pop_all_events_on_idle(NMDhcpNettools *self);
+
+/*****************************************************************************/
+
 static void
 set_error_nettools(GError **error, int r, const char *message)
 {
@@ -156,8 +164,10 @@ lease_option_consume_route(const uint8_t **datap,
 /*****************************************************************************/
 
 static gboolean
-lease_parse_address(NDhcp4ClientLease *lease,
+lease_parse_address(NMDhcpNettools    *self /* for logging context only */,
+                    NDhcp4ClientLease *lease,
                     NML3ConfigData    *l3cd,
+                    const char        *iface,
                     GHashTable        *options,
                     in_addr_t         *out_address,
                     GError           **error)
@@ -228,15 +238,33 @@ lease_parse_address(NDhcp4ClientLease *lease,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_SUBNET_MASK, &l_data, &l_data_len);
-    if (r != 0 || !nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &a_netmask)) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get netmask from lease");
-        return FALSE;
+    if (r == N_DHCP4_E_UNSET) {
+        char str1[NM_UTILS_INET_ADDRSTRLEN];
+        char str2[NM_UTILS_INET_ADDRSTRLEN];
+
+        /* Some DHCP servers may not set the subnet-mask (issue#1037).
+         * Do the same as the dhclient plugin and use a default. */
+        a_plen    = _nm_utils_ip4_get_default_prefix(a_address.s_addr);
+        a_netmask = _nm_utils_ip4_prefix_to_netmask(a_plen);
+        _LOGT("missing subnet mask (option 1). Guess %s based on IP address %s",
+              _nm_utils_inet4_ntop(a_netmask, str1),
+              _nm_utils_inet4_ntop(a_address.s_addr, str2));
+    } else {
+        if (r != 0
+            || !nm_dhcp_lease_data_parse_in_addr(l_data,
+                                                 l_data_len,
+                                                 &a_netmask,
+                                                 iface,
+                                                 NM_DHCP_OPTION_DHCP4_SUBNET_MASK)) {
+            nm_utils_error_set_literal(error,
+                                       NM_UTILS_ERROR_UNKNOWN,
+                                       "could not get netmask from lease");
+            return FALSE;
+        }
+        a_plen    = _nm_utils_ip4_netmask_to_prefix(a_netmask);
+        a_netmask = _nm_utils_ip4_prefix_to_netmask(a_plen);
     }
 
-    a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask);
-
     nm_dhcp_option_add_option_in_addr(options,
                                       AF_INET,
                                       NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS,
@@ -282,6 +310,7 @@ lease_parse_address(NDhcp4ClientLease *lease,
 static void
 lease_parse_address_list(NDhcp4ClientLease       *lease,
                          NML3ConfigData          *l3cd,
+                         const char              *iface,
                          NMDhcpOptionDhcp4Options option,
                          GHashTable              *options,
                          NMStrBuf                *sbuf)
@@ -294,8 +323,14 @@ lease_parse_address_list(NDhcp4ClientLease       *lease,
     if (r != 0)
         return;
 
-    if (l_data_len == 0 || l_data_len % 4 != 0)
+    if (l_data_len == 0 || l_data_len % 4 != 0) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         AF_INET,
+                                         option,
+                                         "wrong option length %lu",
+                                         (unsigned long) l_data_len);
         return;
+    }
 
     nm_str_buf_reset(sbuf);
 
@@ -308,9 +343,14 @@ lease_parse_address_list(NDhcp4ClientLease       *lease,
 
         switch (option) {
         case NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER:
-            if (addr == 0 || nm_ip4_addr_is_localhost(addr)) {
+            if (addr == 0 || nm_utils_ip4_address_is_loopback(addr)) {
                 /* Skip localhost addresses, like also networkd does.
                  * See https://github.com/systemd/systemd/issues/4524. */
+                nm_dhcp_lease_log_invalid_option(iface,
+                                                 AF_INET,
+                                                 option,
+                                                 "address %s is ignored",
+                                                 _nm_utils_inet4_ntop(addr, addr_str));
                 continue;
             }
             nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr);
@@ -502,7 +542,10 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 }
 
 static void
-lease_parse_search_domains(NDhcp4ClientLease *lease, NML3ConfigData *l3cd, GHashTable *options)
+lease_parse_search_domains(NDhcp4ClientLease *lease,
+                           NML3ConfigData    *l3cd,
+                           const char        *iface,
+                           GHashTable        *options)
 {
     gs_strfreev char **domains = NULL;
     const guint8      *l_data;
@@ -514,7 +557,11 @@ lease_parse_search_domains(NDhcp4ClientLease *lease, NML3ConfigData *l3cd, GHash
     if (r != 0)
         return;
 
-    domains = nm_dhcp_lease_data_parse_search_list(l_data, l_data_len);
+    domains = nm_dhcp_lease_data_parse_search_list(l_data,
+                                                   l_data_len,
+                                                   iface,
+                                                   AF_INET,
+                                                   NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST);
 
     if (!domains || !domains[0])
         return;
@@ -556,12 +603,9 @@ lease_parse_private_options(NDhcp4ClientLease *lease, GHashTable *options)
 }
 
 static NML3ConfigData *
-lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    NDhcp4ClientLease *lease,
-                    GError           **error)
+lease_to_ip4_config(NMDhcpNettools *self, NDhcp4ClientLease *lease, GError **error)
 {
+    const char                             *iface;
     nm_auto_str_buf NMStrBuf                sbuf    = NM_STR_BUF_INIT(0, FALSE);
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
     gs_unref_hashtable GHashTable          *options = NULL;
@@ -574,13 +618,15 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     struct in_addr                          v_inaddr_s;
     int                                     r;
 
-    g_return_val_if_fail(lease != NULL, NULL);
+    nm_assert(lease);
+
+    iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
 
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+    l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
     options = nm_dhcp_option_create_options_dict();
 
-    if (!lease_parse_address(lease, l3cd, options, &lease_address, error))
+    if (!lease_parse_address(self, lease, l3cd, iface, options, &lease_address, error))
         return NULL;
 
     r = n_dhcp4_client_lease_get_server_identifier(lease, &v_inaddr_s);
@@ -592,7 +638,12 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BROADCAST, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_in_addr(l_data, l_data_len, &v_inaddr)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_in_addr(l_data,
+                                            l_data_len,
+                                            &v_inaddr,
+                                            iface,
+                                            NM_DHCP_OPTION_DHCP4_BROADCAST)) {
         nm_dhcp_option_add_option_in_addr(options,
                                           AF_INET,
                                           NM_DHCP_OPTION_DHCP4_BROADCAST,
@@ -601,10 +652,21 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
     lease_parse_routes(lease, l3cd, lease_address, options, &sbuf);
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER, options, &sbuf);
+    lease_parse_address_list(lease,
+                             l3cd,
+                             iface,
+                             NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
+                             options,
+                             &sbuf);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_DOMAIN_NAME)) {
         gs_free const char **domains = NULL;
 
         nm_str_buf_reset(&sbuf);
@@ -620,7 +682,10 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
             for (i = 0; domains[i]; i++) {
                 gs_free char *s = NULL;
 
-                s = nm_dhcp_lease_data_parse_domain_validate(domains[i]);
+                s = nm_dhcp_lease_data_parse_domain_validate(domains[i],
+                                                             iface,
+                                                             AF_INET,
+                                                             NM_DHCP_OPTION_DHCP4_DOMAIN_NAME);
                 if (!s)
                     continue;
 
@@ -638,10 +703,16 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         }
     }
 
-    lease_parse_search_domains(lease, l3cd, options);
+    lease_parse_search_domains(lease, l3cd, iface, options);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_mtu(l_data, l_data_len, &v_u16)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_mtu(l_data,
+                                        l_data_len,
+                                        &v_u16,
+                                        iface,
+                                        AF_INET,
+                                        NM_DHCP_OPTION_DHCP4_INTERFACE_MTU)) {
         nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, v_u16);
         nm_l3_config_data_set_mtu(l3cd, v_u16);
     }
@@ -654,15 +725,26 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     if (r == 0) {
         gs_free char *s = NULL;
 
-        if (nm_dhcp_lease_data_parse_domain(l_data, l_data_len, &s)) {
+        if (nm_dhcp_lease_data_parse_domain(l_data,
+                                            l_data_len,
+                                            &s,
+                                            iface,
+                                            AF_INET,
+                                            NM_DHCP_OPTION_DHCP4_HOST_NAME)) {
             nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s);
         }
     }
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, iface, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_ROOT_PATH, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_ROOT_PATH)) {
         /* https://tools.ietf.org/html/rfc2132#section-3.19
          *
          *   The path is formatted as a character string consisting of
@@ -684,7 +766,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                             NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
                             &l_data,
                             &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY)) {
         /* https://tools.ietf.org/html/draft-ietf-wrec-wpad-01#section-4.4.1
          *
          * We reject NUL characters inside the string (except trailing NULs).
@@ -704,7 +792,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_NIS_DOMAIN)) {
         gs_free char *to_free = NULL;
 
         /* https://tools.ietf.org/html/rfc2132#section-8.1 */
@@ -730,7 +824,13 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME, &l_data, &l_data_len);
-    if (r == 0 && nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
+    if (r == 0
+        && nm_dhcp_lease_data_parse_cstr(l_data,
+                                         l_data_len,
+                                         &l_data_len,
+                                         iface,
+                                         AF_INET,
+                                         NM_DHCP_OPTION_DHCP4_BOOTFILE_NAME)) {
         gs_free char *to_free = NULL;
 
         v_str = nm_utils_buf_utf8safe_escape((char *) l_data,
@@ -743,9 +843,14 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                                   v_str ?: "");
     }
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, iface, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
 
-    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, options, &sbuf);
+    lease_parse_address_list(lease,
+                             l3cd,
+                             iface,
+                             NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER,
+                             options,
+                             &sbuf);
 
     lease_parse_private_options(lease, options);
 
@@ -785,9 +890,7 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
 static void
 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;
+    NMDhcpNettoolsPrivate                  *priv  = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
     gs_free_error GError                   *error = NULL;
 
@@ -796,17 +899,14 @@ bound4_handle(NMDhcpNettools *self, guint event, NDhcp4ClientLease *lease)
 
     _LOGT("lease available (%s)", (event == N_DHCP4_CLIENT_EVENT_GRANTED) ? "granted" : "extended");
 
-    client_config = nm_dhcp_client_get_config(client);
-    l3cd          = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(client),
-                               client_config->iface,
-                               nm_dhcp_client_get_ifindex(client),
-                               lease,
-                               &error);
+    l3cd = lease_to_ip4_config(self, lease, &error);
     if (!l3cd) {
         _LOGW("failure to parse lease: %s", error->message);
 
-        if (event == N_DHCP4_CLIENT_EVENT_GRANTED)
+        if (event == N_DHCP4_CLIENT_EVENT_GRANTED) {
             n_dhcp4_client_lease_decline(lease, "invalid lease");
+            dhcp4_event_pop_all_events_on_idle(self);
+        }
 
         _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
         return;
@@ -854,7 +954,7 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
     case N_DHCP4_CLIENT_EVENT_OFFER:
         r = n_dhcp4_client_lease_get_server_identifier(event->offer.lease, &server_id);
         if (r) {
-            _LOGW("selecting lease failed: %d", r);
+            _LOGW("selecting lease failed: could not get DHCP server identifier (%d)", r);
             return;
         }
 
@@ -870,11 +970,19 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
             return;
         }
 
+        if (!_nm_dhcp_client_accept_offer(NM_DHCP_CLIENT(self), &yiaddr.s_addr)) {
+            /* We don't log about this, the parent class is expected to notify about the reasons. */
+            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);
+
+        dhcp4_event_pop_all_events_on_idle(self);
+
         if (r) {
             _LOGW("selecting lease failed: %d", r);
             return;
@@ -906,22 +1014,79 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
     }
 }
 
+static void
+dhcp4_event_pop_all_events(NMDhcpNettools *self)
+{
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    NDhcp4ClientEvent     *event;
+
+    while (!n_dhcp4_client_pop_event(priv->client, &event) && event)
+        dhcp4_event_handle(self, event);
+
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
+}
+
+static gboolean
+dhcp4_event_pop_all_events_on_idle_cb(gpointer user_data)
+{
+    NMDhcpNettools        *self = user_data;
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
+    dhcp4_event_pop_all_events(self);
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+dhcp4_event_pop_all_events_on_idle(NMDhcpNettools *self)
+{
+    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+
+    /* For the most part, NDhcp4Client gets driven from internal, that is
+     * by having events ready on the socket or the timerfd. For those
+     * events, we will poll on the (epoll) FD, then let it be processed
+     * by n_dhcp4_client_dispatch(), and pop the queued events.
+     *
+     * But certain commands (n_dhcp4_client_lease_select(), n_dhcp4_client_lease_accept(),
+     * n_dhcp4_client_lease_decline()) are initiated by the user. And they tend
+     * to log events. Logging is done by queuing a message, but that won't be processed,
+     * unless we pop the event.
+     *
+     * To ensure that those logging events get popped, schedule an idle handler to do that.
+     *
+     * Yes, this means, that the messages only get logged later, when the idle handler
+     * runs. The alternative seems even more problematic, because we don't know
+     * the current call-state, and it seems dangerous to pop unexpected events.
+     * E.g. we call n_dhcp4_client_lease_select() from inside the event-handler,
+     * it seems wrong to call dhcp4_event_pop_all_events() in that context again.
+     *
+     * See-also: https://github.com/nettools/n-dhcp4/issues/34
+     */
+
+    if (!priv->pop_all_events_on_idle_source) {
+        priv->pop_all_events_on_idle_source =
+            nm_g_idle_add_source(dhcp4_event_pop_all_events_on_idle_cb, self);
+    }
+}
+
 static gboolean
 dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
 {
     NMDhcpNettools        *self = user_data;
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    NDhcp4ClientEvent     *event;
     int                    r;
 
     r = n_dhcp4_client_dispatch(priv->client);
     if (r < 0) {
-        /* FIXME: if any operation (e.g. send()) fails during the
+        /* If any operation (e.g. send()) fails during the
          * dispatch, n-dhcp4 returns an error without arming timers
          * or progressing state, so the only reasonable thing to do
          * is to move to failed state so that the client will be
-         * restarted. Ideally n-dhcp4 should retry failed operations
-         * a predefined number of times (possibly infinite).
+         * restarted.
+         *
+         * That means, n_dhcp4_client_dispatch() must not fail if it can
+         * somehow workaround the problem. A failure is really fatal
+         * and the client needs to be restarted.
          */
         _LOGE("error %d dispatching events", r);
         nm_clear_g_source_inst(&priv->event_source);
@@ -929,8 +1094,7 @@ dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
         return G_SOURCE_REMOVE;
     }
 
-    while (!n_dhcp4_client_pop_event(priv->client, &event) && event)
-        dhcp4_event_handle(self, event);
+    dhcp4_event_pop_all_events(self);
 
     return G_SOURCE_CONTINUE;
 }
@@ -1056,6 +1220,8 @@ _accept(NMDhcpClient *client, const NML3ConfigData *l3cd, GError **error)
     if (!r)
         lease_save(self, priv->granted.lease, priv->lease_file);
 
+    dhcp4_event_pop_all_events_on_idle(self);
+
     nm_clear_pointer(&priv->granted.lease, n_dhcp4_client_lease_unref);
     nm_clear_l3cd(&priv->granted.lease_l3cd);
 
@@ -1091,6 +1257,8 @@ decline(NMDhcpClient *client, const NML3ConfigData *l3cd, const char *error_mess
 
     r = n_dhcp4_client_lease_decline(lease, error_message);
 
+    dhcp4_event_pop_all_events_on_idle(self);
+
     if (r) {
         set_error_nettools(error, r, "failed to decline lease");
         return FALSE;
@@ -1155,18 +1323,20 @@ ip4_start(NMDhcpClient *client, GError **error)
     if (client_config->v4.last_address)
         inet_pton(AF_INET, client_config->v4.last_address, &last_addr);
     else {
-        /*
-         * TODO: we stick to the systemd-networkd lease file format. Quite easy for now to
-         * just use the functions in systemd code. Anyway, as in the end we just use the
-         * ip address from all the options found in the lease, write a function that parses
-         * the lease file just for the assigned address and returns it in &last_address.
-         * Then drop reference to systemd-networkd structures and functions.
-         */
-        nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;
-
-        dhcp_lease_load(&lease, lease_file);
-        if (lease)
-            sd_dhcp_lease_get_address(lease, &last_addr);
+        gs_free char *contents = NULL;
+        gs_free char *s_addr   = NULL;
+
+        nm_utils_file_get_contents(-1,
+                                   lease_file,
+                                   64 * 1024,
+                                   NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
+                                   &contents,
+                                   NULL,
+                                   NULL,
+                                   NULL);
+        nm_parse_env_file(contents, "ADDRESS", &s_addr);
+        if (s_addr)
+            nm_utils_parse_inaddr_bin(AF_INET, s_addr, NULL, &last_addr);
     }
 
     if (last_addr.s_addr) {
@@ -1304,6 +1474,7 @@ dispose(GObject *object)
 
     nm_clear_g_free(&priv->lease_file);
     nm_clear_g_source_inst(&priv->event_source);
+    nm_clear_g_source_inst(&priv->pop_all_events_on_idle_source);
     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);
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index f2dd1823..49e21d97 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -25,7 +25,6 @@
 #include "libnm-platform/nm-platform.h"
 #include "nm-dhcp-client-logging.h"
 #include "libnm-systemd-core/nm-sd.h"
-#include "libnm-systemd-core/nm-sd-utils-dhcp.h"
 
 /*****************************************************************************/
 
@@ -47,7 +46,6 @@ static GType nm_dhcp_systemd_get_type(void);
 /*****************************************************************************/
 
 typedef struct {
-    sd_dhcp_client  *client4;
     sd_dhcp6_client *client6;
     char            *lease_file;
 
@@ -70,683 +68,7 @@ G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT)
 /*****************************************************************************/
 
 static NML3ConfigData *
-lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    sd_dhcp_lease     *lease,
-                    GError           **error)
-{
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
-    gs_unref_hashtable GHashTable          *options = NULL;
-    const struct in_addr                   *addr_list;
-    char                                    addr_str[NM_UTILS_INET_ADDRSTRLEN];
-    const char                             *s;
-    nm_auto_free_gstring GString           *str              = NULL;
-    nm_auto_free sd_dhcp_route            **routes_static    = NULL;
-    nm_auto_free sd_dhcp_route            **routes_classless = NULL;
-    const char *const                      *search_domains   = NULL;
-    guint32                                 default_route_metric_offset;
-    guint16                                 mtu;
-    int                                     i;
-    int                                     num;
-    int                                     is_classless;
-    int                                     n_routes_static;
-    int                                     n_routes_classless;
-    const void                             *data;
-    gsize                                   data_len;
-    gboolean                                has_router_from_classless = FALSE;
-    const gint32                            ts      = nm_utils_get_monotonic_timestamp_sec();
-    gint64                                  ts_time = time(NULL);
-    struct in_addr                          a_address;
-    struct in_addr                          a_netmask;
-    struct in_addr                          a_next_server;
-    struct in_addr                          server_id;
-    struct in_addr                          broadcast;
-    const struct in_addr                   *a_router;
-    guint32                                 a_plen;
-    guint32                                 a_lifetime;
-    guint32                                 renewal;
-    guint32                                 rebinding;
-    gs_free nm_sd_dhcp_option              *private_options = NULL;
-
-    nm_assert(lease != NULL);
-
-    if (sd_dhcp_lease_get_address(lease, &a_address) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get address from lease");
-        return NULL;
-    }
-
-    if (sd_dhcp_lease_get_netmask(lease, &a_netmask) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get netmask from lease");
-        return NULL;
-    }
-
-    if (sd_dhcp_lease_get_lifetime(lease, &a_lifetime) < 0) {
-        nm_utils_error_set_literal(error,
-                                   NM_UTILS_ERROR_UNKNOWN,
-                                   "could not get lifetime from lease");
-        return NULL;
-    }
-
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
-
-    options = nm_dhcp_option_create_options_dict();
-
-    _nm_utils_inet4_ntop(a_address.s_addr, addr_str);
-    nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_IP_ADDRESS, addr_str);
-
-    a_plen = nm_utils_ip4_netmask_to_prefix(a_netmask.s_addr);
-    nm_dhcp_option_add_option(options,
-                              AF_INET,
-                              NM_DHCP_OPTION_DHCP4_SUBNET_MASK,
-                              _nm_utils_inet4_ntop(a_netmask.s_addr, addr_str));
-
-    nm_dhcp_option_add_option_u64(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_IP_ADDRESS_LEASE_TIME,
-                                  a_lifetime);
-    nm_dhcp_option_add_option_u64(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_NM_EXPIRY,
-                                  (guint64) (ts_time + a_lifetime));
-
-    if (sd_dhcp_lease_get_next_server(lease, &a_next_server) == 0) {
-        _nm_utils_inet4_ntop(a_next_server.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, addr_str);
-    }
-
-    nm_l3_config_data_add_address_4(l3cd,
-                                    &((const NMPlatformIP4Address){
-                                        .address      = a_address.s_addr,
-                                        .peer_address = a_address.s_addr,
-                                        .plen         = a_plen,
-                                        .addr_source  = NM_IP_CONFIG_SOURCE_DHCP,
-                                        .timestamp    = ts,
-                                        .lifetime     = a_lifetime,
-                                        .preferred    = a_lifetime,
-                                    }));
-
-    if (sd_dhcp_lease_get_server_identifier(lease, &server_id) >= 0) {
-        _nm_utils_inet4_ntop(server_id.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_SERVER_ID, addr_str);
-    }
-
-    if (sd_dhcp_lease_get_broadcast(lease, &broadcast) >= 0) {
-        _nm_utils_inet4_ntop(broadcast.s_addr, addr_str);
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_BROADCAST, addr_str);
-    }
-
-    num = sd_dhcp_lease_get_dns(lease, &addr_list);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-
-            if (addr_list[i].s_addr == 0 || nm_ip4_addr_is_localhost(addr_list[i].s_addr)) {
-                /* Skip localhost addresses, like also networkd does.
-                 * See https://github.com/systemd/systemd/issues/4524. */
-                continue;
-            }
-            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr_list[i].s_addr);
-        }
-        nm_dhcp_option_add_option(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
-                                  str->str);
-    }
-
-    num = sd_dhcp_lease_get_search_domains(lease, (char ***) &search_domains);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            g_string_append(nm_gstring_add_space_delimiter(str), search_domains[i]);
-            nm_l3_config_data_add_search(l3cd, AF_INET, search_domains[i]);
-        }
-        nm_dhcp_option_add_option(options,
-                                  AF_INET,
-                                  NM_DHCP_OPTION_DHCP4_DOMAIN_SEARCH_LIST,
-                                  str->str);
-    }
-
-    if (sd_dhcp_lease_get_domainname(lease, &s) >= 0) {
-        gs_strfreev char **domains = NULL;
-        char             **d;
-
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, s);
-
-        /* Multiple domains sometimes stuffed into option 15 "Domain Name".
-         * As systemd escapes such characters, split them at \\032. */
-        domains = g_strsplit(s, "\\032", 0);
-        for (d = domains; *d; d++)
-            nm_l3_config_data_add_domain(l3cd, AF_INET, *d);
-    }
-
-    if (sd_dhcp_lease_get_hostname(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_HOST_NAME, s);
-    }
-
-    default_route_metric_offset = 0;
-    n_routes_static             = sd_dhcp_lease_get_static_routes(lease, &routes_static);
-    n_routes_classless          = sd_dhcp_lease_get_classless_routes(lease, &routes_classless);
-    for (is_classless = 1; is_classless >= 0; is_classless--) {
-        int                   n_routes = (is_classless ? n_routes_classless : n_routes_static);
-        sd_dhcp_route *const *routes   = (is_classless ? routes_classless : routes_static);
-
-        if (n_routes <= 0)
-            continue;
-
-        nm_gstring_prepare(&str);
-
-        for (i = 0; i < n_routes; i++) {
-            char           network_net_str[NM_UTILS_INET_ADDRSTRLEN];
-            char           gateway_str[NM_UTILS_INET_ADDRSTRLEN];
-            guint8         r_plen;
-            struct in_addr r_network;
-            struct in_addr r_gateway;
-            in_addr_t      network_net;
-            guint32        m;
-
-            if (sd_dhcp_route_get_destination(routes[i], &r_network) < 0)
-                continue;
-            if (sd_dhcp_route_get_destination_prefix_length(routes[i], &r_plen) < 0 || r_plen > 32)
-                continue;
-            if (sd_dhcp_route_get_gateway(routes[i], &r_gateway) < 0)
-                continue;
-
-            network_net = nm_utils_ip4_address_clear_host_address(r_network.s_addr, r_plen);
-            _nm_utils_inet4_ntop(network_net, network_net_str);
-            _nm_utils_inet4_ntop(r_gateway.s_addr, gateway_str);
-
-            g_string_append_printf(nm_gstring_add_space_delimiter(str),
-                                   "%s/%d %s",
-                                   network_net_str,
-                                   (int) r_plen,
-                                   gateway_str);
-
-            if (!is_classless && n_routes_classless > 0) {
-                /* RFC 3443: if the DHCP server returns both a Classless Static Routes
-                 * option and a Static Routes option, the DHCP client MUST ignore the
-                 * Static Routes option. */
-                continue;
-            }
-
-            if (r_plen == 0) {
-                if (!is_classless) {
-                    /* for option 33 (static route), RFC 2132 says:
-                     *
-                     * The default route (0.0.0.0) is an illegal destination for a static
-                     * route. */
-                    continue;
-                }
-
-                /* if there are multiple default routes, we add them with differing
-                 * metrics. */
-                m                         = default_route_metric_offset++;
-                has_router_from_classless = TRUE;
-            } else
-                m = 0;
-
-            nm_l3_config_data_add_route_4(l3cd,
-                                          &((const NMPlatformIP4Route){
-                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                                              .network       = network_net,
-                                              .plen          = r_plen,
-                                              .gateway       = r_gateway.s_addr,
-                                              .pref_src      = a_address.s_addr,
-                                              .metric_any    = TRUE,
-                                              .metric        = m,
-                                              .table_any     = TRUE,
-                                              .table_coerced = 0,
-                                          }));
-        }
-
-        if (str->len > 0) {
-            nm_dhcp_option_add_option(options,
-                                      AF_INET,
-                                      is_classless ? NM_DHCP_OPTION_DHCP4_CLASSLESS_STATIC_ROUTE
-                                                   : NM_DHCP_OPTION_DHCP4_STATIC_ROUTE,
-                                      str->str);
-        }
-    }
-
-    num = sd_dhcp_lease_get_router(lease, &a_router);
-    if (num > 0) {
-        default_route_metric_offset = 0;
-
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            guint32 m;
-
-            s = _nm_utils_inet4_ntop(a_router[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), s);
-
-            if (a_router[i].s_addr == 0) {
-                /* silently skip 0.0.0.0 */
-                continue;
-            }
-
-            if (has_router_from_classless) {
-                /* If the DHCP server returns both a Classless Static Routes option and a
-                 * Router option, the DHCP client MUST ignore the Router option [RFC 3442].
-                 *
-                 * Be more lenient and ignore the Router option only if Classless Static
-                 * Routes contain a default gateway (as other DHCP backends do).
-                 */
-                continue;
-            }
-
-            /* if there are multiple default routes, we add them with differing
-             * metrics. */
-            m = default_route_metric_offset++;
-
-            nm_l3_config_data_add_route_4(l3cd,
-                                          &((const NMPlatformIP4Route){
-                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                                              .gateway       = a_router[i].s_addr,
-                                              .pref_src      = a_address.s_addr,
-                                              .table_any     = TRUE,
-                                              .table_coerced = 0,
-                                              .metric_any    = TRUE,
-                                              .metric        = m,
-                                          }));
-        }
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROUTER, str->str);
-    }
-
-    if (sd_dhcp_lease_get_mtu(lease, &mtu) >= 0 && mtu) {
-        nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, mtu);
-        nm_l3_config_data_set_mtu(l3cd, mtu);
-    }
-
-    num = sd_dhcp_lease_get_ntp(lease, &addr_list);
-    if (num > 0) {
-        nm_gstring_prepare(&str);
-        for (i = 0; i < num; i++) {
-            _nm_utils_inet4_ntop(addr_list[i].s_addr, addr_str);
-            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-        }
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NTP_SERVER, str->str);
-    }
-
-    if (sd_dhcp_lease_get_root_path(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_ROOT_PATH, s);
-    }
-
-    if (sd_dhcp_lease_get_t1(lease, &renewal) >= 0) {
-        nm_dhcp_option_add_option_u64(options,
-                                      AF_INET,
-                                      NM_DHCP_OPTION_DHCP4_RENEWAL_T1_TIME,
-                                      renewal);
-    }
-
-    if (sd_dhcp_lease_get_t2(lease, &rebinding) >= 0) {
-        nm_dhcp_option_add_option_u64(options,
-                                      AF_INET,
-                                      NM_DHCP_OPTION_DHCP4_REBINDING_T2_TIME,
-                                      rebinding);
-    }
-
-    if (sd_dhcp_lease_get_timezone(lease, &s) >= 0) {
-        nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NEW_TZDB_TIMEZONE, s);
-    }
-
-    if (sd_dhcp_lease_get_vendor_specific(lease, &data, &data_len) >= 0) {
-        if (!!memmem(data, data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")))
-            nm_l3_config_data_set_metered(l3cd, TRUE);
-    }
-
-    num = nm_sd_dhcp_lease_get_private_options(lease, &private_options);
-    if (num > 0) {
-        for (i = 0; i < num; i++) {
-            guint8        code       = private_options[i].code;
-            const guint8 *l_data     = private_options[i].data;
-            gsize         l_data_len = private_options[i].data_len;
-            char         *option_string;
-
-            if (code == NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY) {
-                if (nm_dhcp_lease_data_parse_cstr(l_data, l_data_len, &l_data_len)) {
-                    gs_free char *to_free = NULL;
-                    const char   *escaped;
-
-                    escaped =
-                        nm_utils_buf_utf8safe_escape((char *) l_data, l_data_len, 0, &to_free);
-                    nm_dhcp_option_add_option(options,
-                                              AF_INET,
-                                              NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
-                                              escaped ?: "");
-
-                    nm_l3_config_data_set_proxy_method(l3cd, NM_PROXY_CONFIG_METHOD_AUTO);
-                    nm_l3_config_data_set_proxy_pac_url(l3cd, escaped ?: "");
-                }
-                continue;
-            }
-            if (code == NM_DHCP_OPTION_DHCP4_PRIVATE_CLASSLESS_STATIC_ROUTE) {
-                /* nettools and dhclient parse option 249 (Microsoft Classless Static Route)
-                 * as fallback for routes and ignores them from private options.
-                 *
-                 * The systemd plugin does not, and for consistency with nettools we
-                 * also don't expose it as private option either. */
-                continue;
-            }
-
-            option_string = nm_utils_bin2hexstr_full(l_data, l_data_len, ':', FALSE, NULL);
-            nm_dhcp_option_take_option(options, AF_INET, code, option_string);
-        }
-    }
-
-    nm_dhcp_option_add_requests_to_options(options, AF_INET);
-
-    nm_l3_config_data_set_dhcp_lease_from_options(l3cd, AF_INET, g_steal_pointer(&options));
-
-    return g_steal_pointer(&l3cd);
-}
-
-/*****************************************************************************/
-
-static void
-bound4_handle(NMDhcpSystemd *self, gboolean extended)
-{
-    NMDhcpSystemdPrivate                   *priv  = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const char                             *iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
-    nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
-    sd_dhcp_lease                          *lease = NULL;
-    GError                                 *error = NULL;
-
-    if (sd_dhcp_client_get_lease(priv->client4, &lease) < 0 || !lease) {
-        _LOGW("no lease!");
-        _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
-        return;
-    }
-
-    _LOGD("lease available");
-
-    l3cd = lease_to_ip4_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)),
-                               iface,
-                               nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)),
-                               lease,
-                               &error);
-    if (!l3cd) {
-        _LOGW("%s", error->message);
-        g_clear_error(&error);
-        _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_notify(NM_DHCP_CLIENT(self),
-                           extended ? NM_DHCP_CLIENT_EVENT_TYPE_EXTENDED
-                                    : NM_DHCP_CLIENT_EVENT_TYPE_BOUND,
-                           l3cd);
-}
-
-static int
-dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
-{
-    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(user_data);
-    NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    char                  addr_str[INET_ADDRSTRLEN];
-    sd_dhcp_lease        *lease = NULL;
-    struct in_addr        addr;
-    int                   r;
-
-    nm_assert(priv->client4 == client);
-
-    _LOGD("client event %d", event);
-
-    switch (event) {
-    case SD_DHCP_CLIENT_EVENT_EXPIRED:
-        _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_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:
-        bound4_handle(self, TRUE);
-        break;
-    case SD_DHCP_CLIENT_EVENT_IP_ACQUIRE:
-        bound4_handle(self, FALSE);
-        break;
-    case SD_DHCP_CLIENT_EVENT_SELECTING:
-        r = sd_dhcp_client_get_lease(priv->client4, &lease);
-        if (r < 0)
-            return r;
-        r = sd_dhcp_lease_get_server_identifier(lease, &addr);
-        if (r < 0)
-            return r;
-        if (nm_dhcp_client_server_id_is_rejected(NM_DHCP_CLIENT(user_data), &addr)) {
-            _LOGD("server-id %s is in the reject-list, ignoring",
-                  nm_utils_inet_ntop(AF_INET, &addr, addr_str));
-            return -ENOMSG;
-        }
-        break;
-    case SD_DHCP_CLIENT_EVENT_TRANSIENT_FAILURE:
-        break;
-    default:
-        _LOGW("unhandled DHCP event %d", event);
-        break;
-    }
-
-    return 0;
-}
-
-static gboolean
-ip4_start(NMDhcpClient *client, GError **error)
-{
-    nm_auto(sd_dhcp_client_unrefp) sd_dhcp_client *sd_client = NULL;
-    NMDhcpSystemd                                 *self      = NM_DHCP_SYSTEMD(client);
-    NMDhcpSystemdPrivate                          *priv      = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const NMDhcpClientConfig                      *client_config;
-    gs_free char                                  *lease_file = NULL;
-    GBytes                                        *hwaddr;
-    const uint8_t                                 *hwaddr_arr;
-    gsize                                          hwaddr_len;
-    int                                            arp_type;
-    GBytes                                        *client_id;
-    gs_unref_bytes GBytes                         *client_id_new = NULL;
-    GBytes                                        *vendor_class_identifier;
-    const uint8_t                                 *client_id_arr;
-    size_t                                         client_id_len;
-    struct in_addr                                 last_addr = {0};
-    const char                                    *hostname;
-    const char                                    *mud_url;
-    int                                            r, i;
-    GBytes                                        *bcast_hwaddr;
-    const uint8_t                                 *bcast_hwaddr_arr;
-    gsize                                          bcast_hwaddr_len;
-
-    g_return_val_if_fail(!priv->client4, FALSE);
-    g_return_val_if_fail(!priv->client6, FALSE);
-
-    client_config = nm_dhcp_client_get_config(client);
-
-    /* TODO: honor nm_dhcp_client_get_anycast_address() */
-
-    r = sd_dhcp_client_new(&sd_client, FALSE);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to create dhcp-client: %s");
-        return FALSE;
-    }
-
-    _LOGT("dhcp-client4: set " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(sd_client));
-
-    r = sd_dhcp_client_attach_event(sd_client, NULL, 0);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to attach event: %s");
-        return FALSE;
-    }
-
-    hwaddr = client_config->hwaddr;
-    if (!hwaddr || !(hwaddr_arr = g_bytes_get_data(hwaddr, &hwaddr_len))
-        || (arp_type = nm_utils_arp_type_detect_from_hwaddrlen(hwaddr_len)) < 0) {
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address");
-        return FALSE;
-    }
-
-    bcast_hwaddr_arr = NULL;
-    bcast_hwaddr     = client_config->bcast_hwaddr;
-    if (bcast_hwaddr) {
-        bcast_hwaddr_arr = g_bytes_get_data(bcast_hwaddr, &bcast_hwaddr_len);
-        if (bcast_hwaddr_len != hwaddr_len)
-            bcast_hwaddr_arr = NULL;
-    }
-
-    r = sd_dhcp_client_set_mac(sd_client,
-                               hwaddr_arr,
-                               bcast_hwaddr_arr,
-                               hwaddr_len,
-                               (guint16) arp_type);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set MAC address: %s");
-        return FALSE;
-    }
-
-    r = sd_dhcp_client_set_ifindex(sd_client, nm_dhcp_client_get_ifindex(client));
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set ifindex: %s");
-        return FALSE;
-    }
-
-    nm_dhcp_utils_get_leasefile_path(AF_INET,
-                                     "internal",
-                                     client_config->iface,
-                                     client_config->uuid,
-                                     &lease_file);
-
-    if (client_config->v4.last_address)
-        inet_pton(AF_INET, client_config->v4.last_address, &last_addr);
-    else {
-        nm_auto(sd_dhcp_lease_unrefp) sd_dhcp_lease *lease = NULL;
-
-        dhcp_lease_load(&lease, lease_file);
-        if (lease)
-            sd_dhcp_lease_get_address(lease, &last_addr);
-    }
-
-    r = sd_dhcp_client_set_request_broadcast(sd_client, client_config->v4.request_broadcast);
-    nm_assert(r >= 0);
-
-    if (last_addr.s_addr) {
-        r = sd_dhcp_client_set_request_address(sd_client, &last_addr);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set last IPv4 address: %s");
-            return FALSE;
-        }
-    }
-
-    client_id = client_config->client_id;
-    if (!client_id) {
-        client_id_new = nm_utils_dhcp_client_id_mac(arp_type, hwaddr_arr, hwaddr_len);
-        client_id     = client_id_new;
-    }
-
-    if (!(client_id_arr = g_bytes_get_data(client_id, &client_id_len)) || client_id_len < 2) {
-        /* invalid client-ids are not expected. */
-        nm_assert_not_reached();
-
-        nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "no valid IPv4 client-id");
-        return FALSE;
-    }
-
-    /* Note that we always set a client-id. In particular for infiniband that is necessary,
-     * see https://tools.ietf.org/html/rfc4390#section-2.1 . */
-    r = sd_dhcp_client_set_client_id(sd_client,
-                                     client_id_arr[0],
-                                     client_id_arr + 1,
-                                     NM_MIN(client_id_len - 1, _NM_MAX_CLIENT_ID_LEN));
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set IPv4 client-id: %s");
-        return FALSE;
-    }
-
-    /* Add requested options */
-    for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_option_dhcp4_options); i++) {
-        if (_nm_dhcp_option_dhcp4_options[i].include) {
-            nm_assert(_nm_dhcp_option_dhcp4_options[i].option_num <= 255);
-            r = sd_dhcp_client_set_request_option(sd_client,
-                                                  _nm_dhcp_option_dhcp4_options[i].option_num);
-            nm_assert(r >= 0 || r == -EEXIST);
-        }
-    }
-
-    hostname = client_config->hostname;
-    if (hostname) {
-        /* FIXME: sd-dhcp decides which hostname/FQDN option to send (12 or 81)
-         * only based on whether the hostname has a domain part or not. At the
-         * moment there is no way to force one or another.
-         */
-        r = sd_dhcp_client_set_hostname(sd_client, hostname);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s");
-            return FALSE;
-        }
-    }
-
-    mud_url = client_config->mud_url;
-    if (mud_url) {
-        r = sd_dhcp_client_set_mud_url(sd_client, mud_url);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP MUDURL: %s");
-            return FALSE;
-        }
-    }
-
-    vendor_class_identifier = client_config->vendor_class_identifier;
-    if (vendor_class_identifier) {
-        const char *option_data;
-        gsize       len;
-
-        option_data = g_bytes_get_data(vendor_class_identifier, &len);
-        nm_assert(option_data);
-        nm_assert(len <= 255);
-
-        option_data = nm_strndup_a(300, option_data, len, NULL);
-
-        r = sd_dhcp_client_set_vendor_class_identifier(sd_client, option_data);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to set DHCP vendor class identifier: %s");
-            return FALSE;
-        }
-    }
-
-    r = sd_dhcp_client_set_callback(sd_client, dhcp_event_cb, client);
-    if (r < 0) {
-        nm_utils_error_set_errno(error, r, "failed to set callback: %s");
-        return FALSE;
-    }
-
-    priv->client4 = g_steal_pointer(&sd_client);
-
-    g_free(priv->lease_file);
-    priv->lease_file = g_steal_pointer(&lease_file);
-
-    nm_dhcp_client_set_effective_client_id(client, client_id);
-
-    r = sd_dhcp_client_start(priv->client4);
-    if (r < 0) {
-        sd_dhcp_client_set_callback(priv->client4, NULL, NULL);
-        nm_clear_pointer(&priv->client4, sd_dhcp_client_unref);
-        nm_utils_error_set_errno(error, r, "failed to start DHCP client: %s");
-        return FALSE;
-    }
-
-    return TRUE;
-}
-
-static NML3ConfigData *
-lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
-                    const char        *iface,
-                    int                ifindex,
-                    sd_dhcp6_lease    *lease,
-                    gboolean           info_only,
-                    gint32             ts,
-                    GError           **error)
+lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GError **error)
 {
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
     gs_unref_hashtable GHashTable          *options = NULL;
@@ -762,11 +84,11 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
 
     nm_assert(lease);
 
-    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+    l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
     options = nm_dhcp_option_create_options_dict();
 
-    if (!info_only) {
+    if (!nm_dhcp_client_get_config(NM_DHCP_CLIENT(self))->v6.info_only) {
         gboolean has_any_addresses = FALSE;
         uint32_t lft_pref;
         uint32_t lft_valid;
@@ -864,17 +186,13 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
 static void
 bound6_handle(NMDhcpSystemd *self)
 {
-    NMDhcpSystemdPrivate                   *priv  = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
-    const gint32                            ts    = nm_utils_get_monotonic_timestamp_sec();
-    const char                             *iface = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
-    const NMDhcpClientConfig               *client_config;
+    NMDhcpSystemdPrivate                   *priv   = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
+    const gint32                            ts     = nm_utils_get_monotonic_timestamp_sec();
     nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
     gs_free_error GError                   *error  = NULL;
     NMPlatformIP6Address                    prefix = {0};
     sd_dhcp6_lease                         *lease  = NULL;
 
-    client_config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));
-
     if (sd_dhcp6_client_get_lease(priv->client6, &lease) < 0 || !lease) {
         _LOGW(" no lease!");
         _nm_dhcp_client_notify(NM_DHCP_CLIENT(self), NM_DHCP_CLIENT_EVENT_TYPE_FAIL, NULL);
@@ -883,13 +201,7 @@ bound6_handle(NMDhcpSystemd *self)
 
     _LOGD("lease available");
 
-    l3cd = lease_to_ip6_config(nm_dhcp_client_get_multi_idx(NM_DHCP_CLIENT(self)),
-                               iface,
-                               nm_dhcp_client_get_ifindex(NM_DHCP_CLIENT(self)),
-                               lease,
-                               client_config->v6.info_only,
-                               ts,
-                               &error);
+    l3cd = lease_to_ip6_config(self, lease, ts, &error);
 
     if (!l3cd) {
         _LOGW("%s", error->message);
@@ -953,7 +265,6 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
     GBytes                                          *duid;
     gboolean                                         prefix_delegation;
 
-    g_return_val_if_fail(!priv->client4, FALSE);
     g_return_val_if_fail(!priv->client6, FALSE);
 
     client_config = nm_dhcp_client_get_config(client);
@@ -1080,18 +391,13 @@ stop(NMDhcpClient *client, gboolean release)
 
     NM_DHCP_CLIENT_CLASS(nm_dhcp_systemd_parent_class)->stop(client, release);
 
-    _LOGT("dhcp-client%d: stop %p",
-          priv->client4 ? '4' : '6',
-          priv->client4 ? (gpointer) priv->client4 : (gpointer) priv->client6);
+    _LOGT("dhcp-client6: stop");
 
-    if (priv->client4) {
-        sd_dhcp_client_set_callback(priv->client4, NULL, NULL);
-        r = sd_dhcp_client_stop(priv->client4);
-    } else if (priv->client6) {
-        sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
-        r = sd_dhcp6_client_stop(priv->client6);
-    }
+    if (!priv->client6)
+        return;
 
+    sd_dhcp6_client_set_callback(priv->client6, NULL, NULL);
+    r = sd_dhcp6_client_stop(priv->client6);
     if (r)
         _LOGW("failed to stop client (%d)", r);
 }
@@ -1109,12 +415,6 @@ dispose(GObject *object)
 
     nm_clear_g_free(&priv->lease_file);
 
-    if (priv->client4) {
-        sd_dhcp_client_stop(priv->client4);
-        sd_dhcp_client_unref(priv->client4);
-        priv->client4 = NULL;
-    }
-
     if (priv->client6) {
         sd_dhcp6_client_stop(priv->client6);
         sd_dhcp6_client_unref(priv->client6);
@@ -1132,14 +432,13 @@ nm_dhcp_systemd_class_init(NMDhcpSystemdClass *sdhcp_class)
 
     object_class->dispose = dispose;
 
-    client_class->ip4_start = ip4_start;
     client_class->ip6_start = ip6_start;
     client_class->stop      = stop;
 }
 
 const NMDhcpClientFactory _nm_dhcp_client_factory_systemd = {
     .name         = "systemd",
-    .get_type_4   = nm_dhcp_systemd_get_type,
+    .get_type_4   = nm_dhcp_nettools_get_type,
     .get_type_6   = nm_dhcp_systemd_get_type,
     .undocumented = TRUE,
 };
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 88fe83f3..1bffb3c4 100644
--- a/src/core/dhcp/nm-dhcp-utils.c
+++ b/src/core/dhcp/nm-dhcp-utils.c
@@ -14,6 +14,7 @@
 #include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "nm-dhcp-utils.h"
+#include "nm-dhcp-options.h"
 #include "nm-l3-config-data.h"
 #include "nm-utils.h"
 #include "nm-config.h"
@@ -295,7 +296,7 @@ process_classful_routes(const char     *iface,
         return;
 
     if ((NM_PTRARRAY_LEN(searches) % 2) != 0) {
-        _LOG2I(LOGD_DHCP, iface, "  static routes provided, but invalid");
+        _LOG2I(LOGD_DHCP4, iface, "  static routes provided, but invalid");
         return;
     }
 
@@ -305,11 +306,11 @@ process_classful_routes(const char     *iface,
         guint32            rt_addr, rt_route;
 
         if (inet_pton(AF_INET, *s, &rt_addr) <= 0) {
-            _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route address: '%s'", *s);
+            _LOG2W(LOGD_DHCP4, iface, "DHCP provided invalid static route address: '%s'", *s);
             continue;
         }
         if (inet_pton(AF_INET, *(s + 1), &rt_route) <= 0) {
-            _LOG2W(LOGD_DHCP, iface, "DHCP provided invalid static route gateway: '%s'", *(s + 1));
+            _LOG2W(LOGD_DHCP4, iface, "DHCP provided invalid static route gateway: '%s'", *(s + 1));
             continue;
         }
 
@@ -340,7 +341,7 @@ process_classful_routes(const char     *iface,
 
         nm_l3_config_data_add_route_4(l3cd, &route);
 
-        _LOG2I(LOGD_DHCP,
+        _LOG2I(LOGD_DHCP4,
                iface,
                "  static route %s",
                nm_platform_ip4_route_to_string(&route, sbuf, sizeof(sbuf)));
@@ -352,6 +353,7 @@ process_domain_search(int addr_family, const char *iface, const char *str, NML3C
 {
     gs_free const char **searches  = NULL;
     gs_free char        *unescaped = NULL;
+    NMLogDomain          logd      = NM_IS_IPv4(addr_family) ? LOGD_DHCP4 : LOGD_DHCP6;
     const char         **s;
     char                *p;
     int                  i;
@@ -373,13 +375,13 @@ process_domain_search(int addr_family, const char *iface, const char *str, NML3C
     } while (*p++);
 
     if (strchr(unescaped, '\\')) {
-        _LOG2W(LOGD_DHCP, iface, "  invalid domain search: '%s'", unescaped);
+        _LOG2W(logd, iface, "  invalid domain search: '%s'", unescaped);
         return;
     }
 
     searches = nm_strsplit_set(unescaped, " ");
     for (s = searches; searches && *s; s++) {
-        _LOG2I(LOGD_DHCP, iface, "  domain search '%s'", *s);
+        _LOG2I(logd, iface, "  domain search '%s'", *s);
         nm_l3_config_data_add_search(l3cd, addr_family, *s);
     }
 }
@@ -414,18 +416,21 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     str = g_hash_table_lookup(options, "ip_address");
     if (!str || !nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &addr))
         return NULL;
+    if (addr == INADDR_ANY)
+        return NULL;
 
     _LOG2I(LOGD_DHCP4, iface, "  address %s", str);
 
     str = g_hash_table_lookup(options, "subnet_mask");
     if (str && (inet_pton(AF_INET, str, &tmp_addr) > 0)) {
-        plen = nm_utils_ip4_netmask_to_prefix(tmp_addr);
+        plen = _nm_utils_ip4_netmask_to_prefix(tmp_addr);
         _LOG2I(LOGD_DHCP4, iface, "  plen %d (%s)", plen, str);
     } else {
         /* Get default netmask for the IP according to appropriate class. */
         plen = _nm_utils_ip4_get_default_prefix(addr);
         _LOG2I(LOGD_DHCP4, iface, "  plen %d (default)", plen);
     }
+
     nm_platform_ip4_address_set_addr(&address, addr, plen);
 
     /* Routes: if the server returns classless static routes, we MUST ignore
@@ -588,7 +593,7 @@ nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options)
 {
     gs_strfreev char   **split_addr = NULL;
     NMPlatformIP6Address address    = {
-        0,
+           0,
     };
     struct in6_addr tmp_addr;
     char           *str = NULL;
@@ -822,26 +827,6 @@ nm_dhcp_utils_get_leasefile_path(int         addr_family,
     return FALSE;
 }
 
-char *
-nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease)
-{
-    const char *start;
-    const char *iaid;
-
-    if (!lease)
-        return NULL;
-
-    iaid = g_hash_table_lookup(lease, "iaid");
-    if (!iaid)
-        return NULL;
-
-    start = g_hash_table_lookup(lease, "life_starts");
-    if (!start)
-        return NULL;
-
-    return g_strdup_printf("%s|%s", iaid, start);
-}
-
 gboolean
 nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
                                     const NML3ConfigData  *l3cd_new,
@@ -902,28 +887,75 @@ nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
 
 /*****************************************************************************/
 
+void
+nm_dhcp_lease_log_invalid_option(const char *iface,
+                                 int         addr_family,
+                                 guint       option,
+                                 const char *fmt,
+                                 ...)
+{
+    const char   *option_name;
+    gs_free char *msg = NULL;
+    va_list       ap;
+
+    option_name = nm_dhcp_option_request_string(addr_family, option);
+
+    va_start(ap, fmt);
+    msg = g_strdup_vprintf(fmt, ap);
+    va_end(ap);
+
+    _LOG2I(NM_IS_IPv4(addr_family) ? LOGD_DHCP4 : LOGD_DHCP6,
+           iface,
+           "error parsing DHCP option %d (%s)%s%s",
+           option,
+           option_name,
+           msg ? ": " : "",
+           msg ?: "");
+}
+
 gboolean
-nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, uint16_t *out_val)
+nm_dhcp_lease_data_parse_u16(const guint8 *data,
+                             gsize         n_data,
+                             uint16_t     *out_val,
+                             const char   *iface,
+                             int           addr_family,
+                             guint         option)
 {
-    if (n_data != 2)
+    if (n_data != 2) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         addr_family,
+                                         option,
+                                         "invalid option length %lu",
+                                         (unsigned long) n_data);
         return FALSE;
+    }
 
     *out_val = unaligned_read_be16(data);
     return TRUE;
 }
 
 gboolean
-nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, uint16_t *out_val)
+nm_dhcp_lease_data_parse_mtu(const guint8 *data,
+                             gsize         n_data,
+                             uint16_t     *out_val,
+                             const char   *iface,
+                             int           addr_family,
+                             guint         option)
 {
     uint16_t mtu;
 
-    if (!nm_dhcp_lease_data_parse_u16(data, n_data, &mtu))
+    if (!nm_dhcp_lease_data_parse_u16(data, n_data, &mtu, iface, addr_family, option))
         return FALSE;
 
     if (mtu < 68) {
         /* https://tools.ietf.org/html/rfc2132#section-5.1:
          *
          * The minimum legal value for the MTU is 68. */
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         addr_family,
+                                         option,
+                                         "value %u is smaller than 68",
+                                         mtu);
         return FALSE;
     }
 
@@ -932,7 +964,12 @@ nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, uint16_t *out_val
 }
 
 gboolean
-nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len)
+nm_dhcp_lease_data_parse_cstr(const guint8 *data,
+                              gsize         n_data,
+                              gsize        *out_new_len,
+                              const char   *iface,
+                              int           addr_family,
+                              guint         option)
 {
     /* WARNING: this function only validates that the string does not contain
      * NUL characters (and ignores trailing NULs). It does not check character
@@ -947,6 +984,10 @@ nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_l
              *
              * https://tools.ietf.org/html/rfc2132#section-2
              * https://github.com/systemd/systemd/issues/1337 */
+            nm_dhcp_lease_log_invalid_option(iface,
+                                             addr_family,
+                                             option,
+                                             "string contains embedded NUL");
             return FALSE;
         }
     }
@@ -956,32 +997,47 @@ nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_l
 }
 
 char *
-nm_dhcp_lease_data_parse_domain_validate(const char *str)
+nm_dhcp_lease_data_parse_domain_validate(const char *str,
+                                         const char *iface,
+                                         int         addr_family,
+                                         guint       option)
 {
     gs_free char *s = NULL;
 
     s = nm_sd_dns_name_normalize(str);
     if (!s)
-        return NULL;
+        goto err;
 
     if (nm_str_is_empty(s) || (s[0] == '.' && s[1] == '\0')) {
         /* root domains are not allowed. */
-        return NULL;
+        goto err;
     }
 
     if (nm_utils_is_localhost(s))
-        return NULL;
+        goto err;
 
     if (!g_utf8_validate(s, -1, NULL)) {
         /* the result must be valid UTF-8. */
-        return NULL;
+        goto err;
     }
 
     return g_steal_pointer(&s);
+err:
+    nm_dhcp_lease_log_invalid_option(iface,
+                                     addr_family,
+                                     option,
+                                     "'%s' is not a valid DNS domain",
+                                     str);
+    return NULL;
 }
 
 gboolean
-nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val)
+nm_dhcp_lease_data_parse_domain(const guint8 *data,
+                                gsize         n_data,
+                                char        **out_val,
+                                const char   *iface,
+                                int           addr_family,
+                                guint         option)
 {
     gs_free char *str1_free = NULL;
     const char   *str1;
@@ -989,7 +1045,7 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
 
     /* this is mostly the same as systemd's lease_parse_domain(). */
 
-    if (!nm_dhcp_lease_data_parse_cstr(data, n_data, &n_data))
+    if (!nm_dhcp_lease_data_parse_cstr(data, n_data, &n_data, iface, addr_family, option))
         return FALSE;
 
     if (n_data == 0) {
@@ -1001,12 +1057,13 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
          *
          * Note that this is *after* we potentially stripped trailing NULs.
          */
+        nm_dhcp_lease_log_invalid_option(iface, addr_family, option, "empty value");
         return FALSE;
     }
 
     str1 = nm_strndup_a(300, (char *) data, n_data, &str1_free);
 
-    s = nm_dhcp_lease_data_parse_domain_validate(str1);
+    s = nm_dhcp_lease_data_parse_domain_validate(str1, iface, addr_family, option);
     if (!s)
         return FALSE;
 
@@ -1015,7 +1072,11 @@ nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val
 }
 
 gboolean
-nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val)
+nm_dhcp_lease_data_parse_in_addr(const guint8 *data,
+                                 gsize         n_data,
+                                 in_addr_t    *out_val,
+                                 const char   *iface,
+                                 guint         option)
 {
     /* - option 1, https://tools.ietf.org/html/rfc2132#section-3.3
      * - option 28, https://tools.ietf.org/html/rfc2132#section-5.3
@@ -1025,8 +1086,14 @@ nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *ou
      * according to RFC 3396 section 7. Therefore, it's possible that a
      * option carrying a IPv4 address has a length > 4.
      */
-    if (n_data < 4)
+    if (n_data < 4) {
+        nm_dhcp_lease_log_invalid_option(iface,
+                                         AF_INET,
+                                         option,
+                                         "invalid address length %lu",
+                                         (unsigned long) n_data);
         return FALSE;
+    }
 
     *out_val = unaligned_read_ne32(data);
     return TRUE;
@@ -1069,7 +1136,8 @@ static char *
 lease_option_print_domain_name(const uint8_t  *cache,
                                size_t         *n_cachep,
                                const uint8_t **datap,
-                               size_t         *n_datap)
+                               size_t         *n_datap,
+                               gboolean       *invalid)
 {
     nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, FALSE);
     const uint8_t           *domain;
@@ -1080,6 +1148,8 @@ lease_option_print_domain_name(const uint8_t  *cache,
     gboolean                 first     = TRUE;
     uint8_t                  c;
 
+    NM_SET_OUT(invalid, FALSE);
+
     /*
      * We are given two adjacent memory regions. The @cache contains alreday parsed
      * domain names, and the @datap contains the remaining data to parse.
@@ -1096,8 +1166,10 @@ lease_option_print_domain_name(const uint8_t  *cache,
      * Note, that each time a jump to an offset is performed, the size of the
      * cache shrinks, so this is guaranteed to terminate.
      */
-    if (cache + n_cache != *datap)
+    if (cache + n_cache != *datap) {
+        NM_SET_OUT(invalid, TRUE);
         return NULL;
+    }
 
     for (;;) {
         if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c)))
@@ -1122,8 +1194,10 @@ lease_option_print_domain_name(const uint8_t  *cache,
             else
                 first = FALSE;
 
-            if (!lease_option_print_label(&sbuf, n_label, domainp, n_domainp))
+            if (!lease_option_print_label(&sbuf, n_label, domainp, n_domainp)) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             break;
         }
@@ -1136,13 +1210,17 @@ lease_option_print_domain_name(const uint8_t  *cache,
              * two high bits are masked out.
              */
 
-            if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c)))
+            if (!nm_dhcp_lease_data_consume(domainp, n_domainp, &c, sizeof(c))) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             offset += c;
 
-            if (offset >= n_cache)
+            if (offset >= n_cache) {
+                NM_SET_OUT(invalid, TRUE);
                 return NULL;
+            }
 
             domain   = cache + offset;
             n_domain = n_cache - offset;
@@ -1154,29 +1232,44 @@ lease_option_print_domain_name(const uint8_t  *cache,
             break;
         }
         default:
+            NM_SET_OUT(invalid, TRUE);
             return NULL;
         }
     }
 }
 
 char **
-nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data)
+nm_dhcp_lease_data_parse_search_list(const guint8 *data,
+                                     gsize         n_data,
+                                     const char   *iface,
+                                     int           addr_family,
+                                     guint         option)
 {
     GPtrArray    *array   = NULL;
     const guint8 *cache   = data;
     gsize         n_cache = 0;
+    guint         i       = 0;
 
     for (;;) {
         gs_free char *s = NULL;
-
-        s = lease_option_print_domain_name(cache, &n_cache, &data, &n_data);
-        if (!s)
+        gboolean      invalid;
+
+        s = lease_option_print_domain_name(cache, &n_cache, &data, &n_data, &invalid);
+        if (!s) {
+            if (iface && invalid)
+                nm_dhcp_lease_log_invalid_option(iface,
+                                                 addr_family,
+                                                 option,
+                                                 "search domain #%u is invalid",
+                                                 i);
             break;
+        }
 
         if (!array)
             array = g_ptr_array_new();
 
         g_ptr_array_add(array, g_steal_pointer(&s));
+        i++;
     }
 
     if (!array)
diff --git a/src/core/dhcp/nm-dhcp-utils.h b/src/core/dhcp/nm-dhcp-utils.h
index bef50c52..00199b02 100644
--- a/src/core/dhcp/nm-dhcp-utils.h
+++ b/src/core/dhcp/nm-dhcp-utils.h
@@ -59,13 +59,48 @@ nm_dhcp_lease_data_consume_in_addr(const uint8_t **datap, size_t *n_datap, in_ad
     return nm_dhcp_lease_data_consume(datap, n_datap, addrp, sizeof(struct in_addr));
 }
 
-char *nm_dhcp_lease_data_parse_domain_validate(const char *str);
-
-gboolean nm_dhcp_lease_data_parse_u16(const guint8 *data, gsize n_data, guint16 *out_val);
-gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, guint16 *out_val);
-gboolean nm_dhcp_lease_data_parse_cstr(const guint8 *data, gsize n_data, gsize *out_new_len);
-gboolean nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val);
-gboolean nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *out_val);
-char   **nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data);
+void     nm_dhcp_lease_log_invalid_option(const char *iface,
+                                          int         addr_family,
+                                          guint       option,
+                                          const char *fmt,
+                                          ...) G_GNUC_PRINTF(4, 5);
+char    *nm_dhcp_lease_data_parse_domain_validate(const char *str,
+                                                  const char *iface,
+                                                  int         addr_family,
+                                                  guint       option);
+gboolean nm_dhcp_lease_data_parse_u16(const guint8 *data,
+                                      gsize         n_data,
+                                      guint16      *out_val,
+                                      const char   *iface,
+                                      int           addr_family,
+                                      guint         option);
+gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data,
+                                      gsize         n_data,
+                                      guint16      *out_val,
+                                      const char   *iface,
+                                      int           addr_family,
+                                      guint         option);
+gboolean nm_dhcp_lease_data_parse_cstr(const guint8 *data,
+                                       gsize         n_data,
+                                       gsize        *out_new_len,
+                                       const char   *iface,
+                                       int           addr_family,
+                                       guint         option);
+gboolean nm_dhcp_lease_data_parse_domain(const guint8 *data,
+                                         gsize         n_data,
+                                         char        **out_val,
+                                         const char   *iface,
+                                         int           addr_family,
+                                         guint         option);
+gboolean nm_dhcp_lease_data_parse_in_addr(const guint8 *data,
+                                          gsize         n_data,
+                                          in_addr_t    *out_val,
+                                          const char   *iface,
+                                          guint         option);
+char   **nm_dhcp_lease_data_parse_search_list(const guint8 *data,
+                                              gsize         n_data,
+                                              const char   *iface,
+                                              int           addr_family,
+                                              guint         option);
 
 #endif /* __NETWORKMANAGER_DHCP_UTILS_H__ */
diff --git a/src/core/dhcp/tests/test-dhcp-utils.c b/src/core/dhcp/tests/test-dhcp-utils.c
index f021b642..7597f516 100644
--- a/src/core/dhcp/tests/test-dhcp-utils.c
+++ b/src/core/dhcp/tests/test-dhcp-utils.c
@@ -196,16 +196,16 @@ test_parse_search_list(void)
     char  **domains;
 
     data    = (guint8[]){0x05, 'l', 'o', 'c', 'a', 'l', 0x00};
-    domains = nm_dhcp_lease_data_parse_search_list(data, 7);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 7, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 1);
     g_assert_cmpstr(domains[0], ==, "local");
     g_strfreev(domains);
 
     data    = (guint8[]){0x04, 't',  'e',  's', 't', 0x07, 'e',  'x',  'a',  'm', 'p', 'l',
-                      'e',  0x03, 'c',  'o', 'm', 0x00, 0xc0, 0x05, 0x03, 'a', 'b', 'c',
-                      0xc0, 0x0d, 0x06, 'f', 'o', 'o',  'b',  'a',  'r',  0x00};
-    domains = nm_dhcp_lease_data_parse_search_list(data, 34);
+                         'e',  0x03, 'c',  'o', 'm', 0x00, 0xc0, 0x05, 0x03, 'a', 'b', 'c',
+                         0xc0, 0x0d, 0x06, 'f', 'o', 'o',  'b',  'a',  'r',  0x00};
+    domains = nm_dhcp_lease_data_parse_search_list(data, 34, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 4);
     g_assert_cmpstr(domains[0], ==, "test.example.com");
@@ -220,7 +220,7 @@ test_parse_search_list(void)
         'a',
         'd',
     };
-    domains = nm_dhcp_lease_data_parse_search_list(data, 4);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 4, NULL, 0, 0);
     g_assert(!domains);
 
     data = (guint8[]){
@@ -235,7 +235,7 @@ test_parse_search_list(void)
         'a',
         'd',
     };
-    domains = nm_dhcp_lease_data_parse_search_list(data, 10);
+    domains = nm_dhcp_lease_data_parse_search_list(data, 10, NULL, 0, 0);
     g_assert(domains);
     g_assert_cmpint(g_strv_length(domains), ==, 1);
     g_assert_cmpstr(domains[0], ==, "okay");
@@ -285,7 +285,7 @@ test_classless_static_routes_1(void)
     const char                              *expected_route2_dest = "10.0.0.0";
     const char                              *expected_route2_gw   = "10.17.66.41";
     static const Option                      data[]               = {
-        /* dhclient custom format */
+                                           /* dhclient custom format */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 8 10 10 17 66 41"},
         {NULL, NULL}};
 
@@ -310,7 +310,7 @@ test_classless_static_routes_2(void)
     const char                              *expected_route2_dest = "10.0.0.0";
     const char                              *expected_route2_gw   = "10.17.66.41";
     static const Option                      data[]               = {
-        /* dhcpcd format */
+                                           /* dhcpcd format */
         {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.0.0/8 10.17.66.41"},
         {NULL, NULL}};
 
@@ -336,9 +336,9 @@ test_fedora_dhclient_classless_static_routes(void)
     const char                              *expected_route2_gw   = "10.34.255.6";
     const char                              *expected_gateway     = "192.168.0.113";
     static const Option                      data[]               = {
-        /* Fedora dhclient format */
+                                           /* Fedora dhclient format */
         {"classless_static_routes",
-         "0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6"},
+                                            "0 192.168.0.113 25.129.210.177.132 192.168.0.113 7.2 10.34.255.6"},
         {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
@@ -362,7 +362,7 @@ test_dhclient_invalid_classless_routes_1(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static const Option                      data[]               = {
-        /* dhclient format */
+                                           /* dhclient format */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 45 10 17 66 41"},
         {NULL, NULL}};
 
@@ -389,7 +389,7 @@ test_dhcpcd_invalid_classless_routes_1(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-        /* dhcpcd format */
+                                           /* dhcpcd format */
         {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.adfadf/44 10.17.66.41"},
         {NULL, NULL}};
 
@@ -419,8 +419,8 @@ test_dhclient_invalid_classless_routes_2(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-        {"rfc3442_classless_static_routes", "45 10 17 66 41 24 192 168 10 192 168 1 1"},
-        {NULL, NULL}};
+                                           {"rfc3442_classless_static_routes", "45 10 17 66 41 24 192 168 10 192 168 1 1"},
+                                           {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -448,8 +448,8 @@ test_dhcpcd_invalid_classless_routes_2(void)
     const char                              *expected_route2_dest = "100.99.88.56";
     const char                              *expected_route2_gw   = "10.1.1.1";
     static const Option                      data[]               = {
-        {"classless_static_routes", "10.0.adfadf/44 10.17.66.41 192.168.10.0/24 192.168.1.1"},
-        {NULL, NULL}};
+                                           {"classless_static_routes", "10.0.adfadf/44 10.17.66.41 192.168.10.0/24 192.168.1.1"},
+                                           {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -477,8 +477,8 @@ test_dhclient_invalid_classless_routes_3(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static const Option                      data[]               = {
-        {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 32 128 10 17 66 41"},
-        {NULL, NULL}};
+                                           {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 32 128 10 17 66 41"},
+                                           {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -501,8 +501,8 @@ test_dhcpcd_invalid_classless_routes_3(void)
     const char                              *expected_route1_dest = "192.168.10.0";
     const char                              *expected_route1_gw   = "192.168.1.1";
     static Option                            data[]               = {
-        {"classless_static_routes", "192.168.10.0/24 192.168.1.1 128/32 10.17.66.41"},
-        {NULL, NULL}};
+                                                 {"classless_static_routes", "192.168.10.0/24 192.168.1.1 128/32 10.17.66.41"},
+                                                 {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -526,8 +526,8 @@ test_dhclient_gw_in_classless_routes(void)
     const char                              *expected_route1_gw   = "192.168.1.1";
     const char                              *expected_gateway     = "192.2.3.4";
     static Option                            data[]               = {
-        {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 0 192 2 3 4"},
-        {NULL, NULL}};
+                                                 {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 0 192 2 3 4"},
+                                                 {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
@@ -550,8 +550,8 @@ test_dhcpcd_gw_in_classless_routes(void)
     const char                              *expected_route1_gw   = "192.168.1.1";
     const char                              *expected_gateway     = "192.2.3.4";
     static Option                            data[]               = {
-        {"classless_static_routes", "192.168.10.0/24 192.168.1.1 0.0.0.0/0 192.2.3.4"},
-        {NULL, NULL}};
+                                                 {"classless_static_routes", "192.168.10.0/24 192.168.1.1 0.0.0.0/0 192.2.3.4"},
+                                                 {NULL, NULL}};
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);