summary refs log tree commit diff
path: root/src/core/dhcp
diff options
context:
space:
mode:
Diffstat (limited to 'src/core/dhcp')
-rw-r--r--src/core/dhcp/nm-dhcp-client-logging.h4
-rw-r--r--src/core/dhcp/nm-dhcp-client.c1331
-rw-r--r--src/core/dhcp/nm-dhcp-client.h265
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.c91
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.h18
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c209
-rw-r--r--src/core/dhcp/nm-dhcp-dhcpcanon.c42
-rw-r--r--src/core/dhcp/nm-dhcp-dhcpcd.c30
-rw-r--r--src/core/dhcp/nm-dhcp-helper.c20
-rw-r--r--src/core/dhcp/nm-dhcp-listener.c32
-rw-r--r--src/core/dhcp/nm-dhcp-manager.c382
-rw-r--r--src/core/dhcp/nm-dhcp-manager.h48
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c420
-rw-r--r--src/core/dhcp/nm-dhcp-options.c23
-rw-r--r--src/core/dhcp/nm-dhcp-options.h5
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c513
-rw-r--r--src/core/dhcp/nm-dhcp-utils.c367
-rw-r--r--src/core/dhcp/nm-dhcp-utils.h35
-rw-r--r--src/core/dhcp/tests/test-dhcp-dhclient.c45
-rw-r--r--src/core/dhcp/tests/test-dhcp-utils.c581
20 files changed, 2029 insertions, 2432 deletions
diff --git a/src/core/dhcp/nm-dhcp-client-logging.h b/src/core/dhcp/nm-dhcp-client-logging.h
index d69b3ebd..2b0d8d06 100644
--- a/src/core/dhcp/nm-dhcp-client-logging.h
+++ b/src/core/dhcp/nm-dhcp-client-logging.h
@@ -39,8 +39,8 @@ _nm_dhcp_client_get_domain(NMDhcpClient *self)
          * expensive to determine the correct value then what we could
          * safe. */                   \
         if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                 \
-            NMDhcpClient *    _self    = (NMDhcpClient *) (self);                        \
-            const char *      __ifname = _self ? nm_dhcp_client_get_iface(_self) : NULL; \
+            NMDhcpClient     *_self    = (NMDhcpClient *) (self);                        \
+            const char       *__ifname = _self ? nm_dhcp_client_get_iface(_self) : NULL; \
             const NMLogDomain _domain  = _nm_dhcp_client_get_domain(_self);              \
                                                                                          \
             nm_log(_level,                                                               \
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index f88c79c0..ec910389 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -20,9 +20,13 @@
 
 #include "NetworkManagerUtils.h"
 #include "nm-utils.h"
+#include "nm-l3cfg.h"
+#include "nm-l3-config-data.h"
 #include "nm-dhcp-utils.h"
 #include "nm-dhcp-options.h"
 #include "libnm-platform/nm-platform.h"
+#include "nm-hostname-manager.h"
+#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "nm-dhcp-client-logging.h"
 
@@ -32,53 +36,24 @@ enum { SIGNAL_NOTIFY, LAST_SIGNAL };
 
 static guint signals[LAST_SIGNAL] = {0};
 
-NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient,
-                             PROP_ADDR_FAMILY,
-                             PROP_ANYCAST_ADDRESS,
-                             PROP_FLAGS,
-                             PROP_HWADDR,
-                             PROP_BROADCAST_HWADDR,
-                             PROP_IFACE,
-                             PROP_IFINDEX,
-                             PROP_MULTI_IDX,
-                             PROP_ROUTE_METRIC,
-                             PROP_ROUTE_TABLE,
-                             PROP_TIMEOUT,
-                             PROP_UUID,
-                             PROP_IAID,
-                             PROP_IAID_EXPLICIT,
-                             PROP_HOSTNAME,
-                             PROP_HOSTNAME_FLAGS,
-                             PROP_MUD_URL,
-                             PROP_VENDOR_CLASS_IDENTIFIER,
-                             PROP_REJECT_SERVERS, );
+NM_GOBJECT_PROPERTIES_DEFINE(NMDhcpClient, PROP_CONFIG, );
 
 typedef struct _NMDhcpClientPrivate {
-    NMDedupMultiIndex * multi_idx;
-    char *              iface;
-    GBytes *            hwaddr;
-    GBytes *            bcast_hwaddr;
-    char *              uuid;
-    GBytes *            client_id;
-    char *              hostname;
-    const char **       reject_servers;
-    char *              mud_url;
-    char *              anycast_address;
-    GBytes *            vendor_class_identifier;
-    pid_t               pid;
-    guint               timeout_id;
-    guint               watch_id;
-    int                 addr_family;
-    int                 ifindex;
-    guint32             route_table;
-    guint32             route_metric;
-    guint32             timeout;
-    guint32             iaid;
-    NMDhcpState         state;
-    NMDhcpHostnameFlags hostname_flags;
-    NMDhcpClientFlags   client_flags;
-    bool                iaid_explicit : 1;
-    bool                is_stopped : 1;
+    NMDhcpClientConfig    config;
+    const NML3ConfigData *l3cd;
+    GSource              *no_lease_timeout_source;
+    GSource              *ipv6_lladdr_timeout_source;
+    GBytes               *effective_client_id;
+    pid_t                 pid;
+    guint                 watch_id;
+    NMDhcpState           state;
+    bool                  iaid_explicit : 1;
+    bool                  is_stopped : 1;
+    struct {
+        gulong id;
+        bool   wait_dhcp_commit : 1;
+        bool   wait_ll_address : 1;
+    } l3cfg_notify;
 } NMDhcpClientPrivate;
 
 G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
@@ -87,6 +62,11 @@ G_DEFINE_ABSTRACT_TYPE(NMDhcpClient, nm_dhcp_client, G_TYPE_OBJECT)
 
 /*****************************************************************************/
 
+static void
+l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self);
+
+/*****************************************************************************/
+
 /* we use pid=-1 for invalid PIDs. Ensure that pid_t can hold negative values. */
 G_STATIC_ASSERT(!(((pid_t) -1) > 0));
 
@@ -98,178 +78,55 @@ _emit_notify(NMDhcpClient *self, const NMDhcpClientNotifyData *notify_data)
     g_signal_emit(G_OBJECT(self), signals[SIGNAL_NOTIFY], 0, notify_data);
 }
 
-static void
-_emit_notify_state_changed(NMDhcpClient *self,
-                           NMDhcpState   dhcp_state,
-                           NMIPConfig *  ip_config,
-                           GHashTable *  options)
-{
-    const NMDhcpClientNotifyData notify_data = {
-        .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_STATE_CHANGED,
-        .state_changed =
-            {
-                .dhcp_state = dhcp_state,
-                .ip_config  = ip_config,
-                .options    = options,
-            },
-    };
-
-    _emit_notify(self, &notify_data);
-}
-
 /*****************************************************************************/
 
-pid_t
-nm_dhcp_client_get_pid(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->pid;
-}
-
-NMDedupMultiIndex *
-nm_dhcp_client_get_multi_idx(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->multi_idx;
-}
-
-const char *
-nm_dhcp_client_get_iface(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->iface;
-}
-
-int
-nm_dhcp_client_get_ifindex(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->ifindex;
-}
-
-int
-nm_dhcp_client_get_addr_family(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), AF_UNSPEC);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->addr_family;
-}
-
-const char *
-nm_dhcp_client_get_uuid(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->uuid;
-}
-
-GBytes *
-nm_dhcp_client_get_hw_addr(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->hwaddr;
-}
-
-GBytes *
-nm_dhcp_client_get_broadcast_hw_addr(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->bcast_hwaddr;
-}
-
-guint32
-nm_dhcp_client_get_route_table(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), RT_TABLE_MAIN);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->route_table;
-}
-
-void
-nm_dhcp_client_set_route_table(NMDhcpClient *self, guint32 route_table)
+static void
+connect_l3cfg_notify(NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gboolean             do_connect;
 
-    if (route_table != priv->route_table) {
-        priv->route_table = route_table;
-        _notify(self, PROP_ROUTE_TABLE);
-    }
-}
-
-guint32
-nm_dhcp_client_get_route_metric(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), G_MAXUINT32);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->route_metric;
-}
-
-void
-nm_dhcp_client_set_route_metric(NMDhcpClient *self, guint32 route_metric)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    do_connect = priv->l3cfg_notify.wait_dhcp_commit | priv->l3cfg_notify.wait_ll_address;
 
-    if (route_metric != priv->route_metric) {
-        priv->route_metric = route_metric;
-        _notify(self, PROP_ROUTE_METRIC);
+    if (!do_connect) {
+        nm_clear_g_signal_handler(priv->config.l3cfg, &priv->l3cfg_notify.id);
+        return;
     }
-}
 
-guint32
-nm_dhcp_client_get_timeout(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 0);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->timeout;
-}
-
-guint32
-nm_dhcp_client_get_iaid(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), 0);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->iaid;
-}
-
-gboolean
-nm_dhcp_client_get_iaid_explicit(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->iaid_explicit;
+    if (priv->l3cfg_notify.id == 0) {
+        priv->l3cfg_notify.id = g_signal_connect(priv->config.l3cfg,
+                                                 NM_L3CFG_SIGNAL_NOTIFY,
+                                                 G_CALLBACK(l3_cfg_notify_cb),
+                                                 self);
+    }
 }
 
-GBytes *
-nm_dhcp_client_get_client_id(NMDhcpClient *self)
+pid_t
+nm_dhcp_client_get_pid(NMDhcpClient *self)
 {
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
+    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), -1);
 
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->client_id;
+    return NM_DHCP_CLIENT_GET_PRIVATE(self)->pid;
 }
 
 static void
-_set_client_id(NMDhcpClient *self, GBytes *client_id, gboolean take)
+_set_effective_client_id(NMDhcpClient *self, GBytes *client_id, gboolean take)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
     nm_assert(!client_id || g_bytes_get_size(client_id) >= 2);
 
-    if (priv->client_id == client_id
-        || (priv->client_id && client_id && g_bytes_equal(priv->client_id, client_id))) {
+    if (priv->effective_client_id == client_id
+        || (priv->effective_client_id && client_id
+            && g_bytes_equal(priv->effective_client_id, client_id))) {
         if (take && client_id)
             g_bytes_unref(client_id);
         return;
     }
 
-    if (priv->client_id)
-        g_bytes_unref(priv->client_id);
-    priv->client_id = client_id;
+    if (priv->effective_client_id)
+        g_bytes_unref(priv->effective_client_id);
+    priv->effective_client_id = client_id;
     if (!take && client_id)
         g_bytes_ref(client_id);
 
@@ -277,94 +134,20 @@ _set_client_id(NMDhcpClient *self, GBytes *client_id, gboolean take)
         gs_free char *s = NULL;
 
         _LOGT("%s: set %s",
-              nm_dhcp_client_get_addr_family(self) == AF_INET6 ? "duid" : "client-id",
-              priv->client_id ? (s = nm_dhcp_utils_duid_to_string(priv->client_id)) : "default");
+              priv->config.addr_family == AF_INET6 ? "duid" : "client-id",
+              priv->effective_client_id
+                  ? (s = nm_dhcp_utils_duid_to_string(priv->effective_client_id))
+                  : "default");
     }
 }
 
 void
-nm_dhcp_client_set_client_id(NMDhcpClient *self, GBytes *client_id)
+nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id)
 {
     g_return_if_fail(NM_IS_DHCP_CLIENT(self));
     g_return_if_fail(!client_id || g_bytes_get_size(client_id) >= 2);
 
-    _set_client_id(self, client_id, FALSE);
-}
-
-void
-nm_dhcp_client_set_client_id_bin(NMDhcpClient *self,
-                                 guint8        type,
-                                 const guint8 *client_id,
-                                 gsize         len)
-{
-    guint8 *buf;
-    GBytes *b;
-
-    g_return_if_fail(NM_IS_DHCP_CLIENT(self));
-    g_return_if_fail(client_id);
-    g_return_if_fail(len > 0);
-
-    buf    = g_malloc(len + 1);
-    buf[0] = type;
-    memcpy(buf + 1, client_id, len);
-    b = g_bytes_new_take(buf, len + 1);
-    _set_client_id(self, b, TRUE);
-}
-
-const char *
-nm_dhcp_client_get_anycast_address(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->anycast_address;
-}
-
-const char *
-nm_dhcp_client_get_hostname(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->hostname;
-}
-
-NMDhcpHostnameFlags
-nm_dhcp_client_get_hostname_flags(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NM_DHCP_HOSTNAME_FLAG_NONE);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->hostname_flags;
-}
-
-NMDhcpClientFlags
-nm_dhcp_client_get_client_flags(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NM_DHCP_CLIENT_FLAGS_NONE);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->client_flags;
-}
-
-const char *
-nm_dhcp_client_get_mud_url(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->mud_url;
-}
-
-GBytes *
-nm_dhcp_client_get_vendor_class_identifier(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return NM_DHCP_CLIENT_GET_PRIVATE(self)->vendor_class_identifier;
-}
-
-const char *const *
-nm_dhcp_client_get_reject_servers(NMDhcpClient *self)
-{
-    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), NULL);
-
-    return (const char *const *) NM_DHCP_CLIENT_GET_PRIVATE(self)->reject_servers;
+    _set_effective_client_id(self, client_id, FALSE);
 }
 
 /*****************************************************************************/
@@ -413,14 +196,6 @@ reason_to_state(NMDhcpClient *self, const char *iface, const char *reason)
 /*****************************************************************************/
 
 static void
-timeout_cleanup(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    nm_clear_g_source(&priv->timeout_id);
-}
-
-static void
 watch_cleanup(NMDhcpClient *self)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
@@ -457,95 +232,181 @@ stop(NMDhcpClient *self, gboolean release)
     if (priv->pid > 0) {
         /* Clean up the watch handler since we're explicitly killing the daemon */
         watch_cleanup(self);
-        nm_dhcp_client_stop_pid(priv->pid, priv->iface);
+        nm_dhcp_client_stop_pid(priv->pid, priv->config.iface);
     }
     priv->pid = -1;
 }
 
-void
-nm_dhcp_client_set_state(NMDhcpClient *self,
-                         NMDhcpState   new_state,
-                         NMIPConfig *  ip_config,
-                         GHashTable *  options)
+static gboolean
+_no_lease_timeout(gpointer user_data)
 {
+    NMDhcpClient        *self = user_data;
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) {
-        g_return_if_fail(NM_IS_IP_CONFIG_ADDR_FAMILY(ip_config, priv->addr_family));
-        g_return_if_fail(options);
+    nm_clear_g_source_inst(&priv->no_lease_timeout_source);
+
+    _emit_notify(self,
+                 &((NMDhcpClientNotifyData){
+                     .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT,
+                 }));
+    return G_SOURCE_CONTINUE;
+}
+
+const NMDhcpClientConfig *
+nm_dhcp_client_get_config(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return &priv->config;
+}
+
+static void
+schedule_no_lease_timeout(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    if (priv->no_lease_timeout_source)
+        return;
+
+    if (priv->config.timeout == NM_DHCP_TIMEOUT_INFINITY) {
+        _LOGI("activation: beginning transaction (no timeout)");
+        priv->no_lease_timeout_source = g_source_ref(nm_g_source_sentinel_get(0));
     } else {
-        g_return_if_fail(!ip_config);
-        g_return_if_fail(!options);
+        _LOGI("activation: beginning transaction (timeout in %u seconds)",
+              (guint) priv->config.timeout);
+        priv->no_lease_timeout_source =
+            nm_g_timeout_add_seconds_source(priv->config.timeout, _no_lease_timeout, self);
     }
+}
+
+void
+nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3ConfigData *l3cd)
+{
+    NMDhcpClientPrivate                     *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    GHashTable                              *options;
+    const int                                IS_IPv4     = NM_IS_IPv4(priv->config.addr_family);
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd_merged = NULL;
+
+    g_return_if_fail(NM_IS_DHCP_CLIENT(self));
+
+    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) {
+        g_return_if_fail(NM_IS_L3_CONFIG_DATA(l3cd));
+        g_return_if_fail(nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family));
+    } else
+        g_return_if_fail(!l3cd);
+
+    if (l3cd)
+        nm_l3_config_data_seal(l3cd);
 
-    if (new_state >= NM_DHCP_STATE_BOUND)
-        timeout_cleanup(self);
     if (new_state >= NM_DHCP_STATE_TIMEOUT)
         watch_cleanup(self);
 
-    /* The client may send same-state transitions for RENEW/REBIND events and
-     * the lease may have changed, so handle same-state transitions for the
-     * EXTENDED and BOUND states.  Ignore same-state transitions for other
-     * events since the lease won't have changed and the state was already handled.
-     */
-    if ((priv->state == new_state)
-        && !NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED))
-        return;
+    if (!IS_IPv4 && l3cd) {
+        if (nm_dhcp_utils_merge_new_dhcp6_lease(priv->l3cd, l3cd, &l3cd_merged)) {
+            l3cd = nm_l3_config_data_seal(l3cd_merged);
+        }
+    }
 
-    if (_LOGD_ENABLED()) {
-        gs_free const char **keys = NULL;
-        guint                i, nkeys;
+    if (priv->l3cd == l3cd)
+        return;
 
-        keys = nm_strdict_get_keys(options, TRUE, &nkeys);
-        for (i = 0; i < nkeys; i++) {
-            _LOGD("option %-20s => '%s'", keys[i], (char *) g_hash_table_lookup(options, keys[i]));
-        }
+    if (l3cd) {
+        nm_clear_g_source_inst(&priv->no_lease_timeout_source);
+    } else {
+        if (priv->l3cd)
+            schedule_no_lease_timeout(self);
     }
 
-    if (_LOGT_ENABLED() && priv->addr_family == AF_INET6) {
-        gs_free char *event_id = NULL;
+    /* FIXME(l3cfg:dhcp): the API of NMDhcpClient is changing to expose a simpler API.
+     * The internals like NMDhcpState should not be exposed (or possibly dropped in large
+     * parts). */
+
+    nm_l3_config_data_reset(&priv->l3cd, l3cd);
+
+    options = l3cd ? nm_dhcp_lease_get_options(
+                  nm_l3_config_data_get_dhcp_lease(l3cd, priv->config.addr_family))
+                   : NULL;
+
+    if (_LOGD_ENABLED()) {
+        if (options) {
+            gs_free const char **keys = NULL;
+            guint                nkeys;
+            guint                i;
+
+            keys = nm_strdict_get_keys(options, TRUE, &nkeys);
+            for (i = 0; i < nkeys; i++) {
+                _LOGD("option %-20s => '%s'",
+                      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);
+                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 =
-            NM_IS_IPv4(priv->addr_family)
-                ? 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);
+            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%s",
-              nm_dhcp_state_to_string(priv->state),
-              nm_dhcp_state_to_string(new_state),
+        _LOGI("state changed %s%s%s%s",
+              priv->l3cd ? "new lease" : "no lease",
               NM_PRINT_FMT_QUOTED(addr, ", address=", addr, "", ""));
     }
 
-    priv->state = new_state;
-
-    _emit_notify_state_changed(self, new_state, ip_config, options);
-}
-
-static gboolean
-transaction_timeout(gpointer user_data)
-{
-    NMDhcpClient *       self = NM_DHCP_CLIENT(user_data);
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    /* 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 nm_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 (nm_dhcp_client_can_accept(self) && new_state == NM_DHCP_STATE_BOUND && priv->l3cd
+        && nm_l3_config_data_get_num_addresses(priv->l3cd, priv->config.addr_family) > 0) {
+        priv->l3cfg_notify.wait_dhcp_commit = TRUE;
+    } else {
+        priv->l3cfg_notify.wait_dhcp_commit = FALSE;
+    }
+    connect_l3cfg_notify(self);
 
-    priv->timeout_id = 0;
-    _LOGW("request timed out");
-    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TIMEOUT, NULL, NULL);
-    return G_SOURCE_REMOVE;
+    {
+        const NMDhcpClientNotifyData notify_data = {
+            .notify_type = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+            .lease_update =
+                {
+                    .l3cd     = priv->l3cd,
+                    .accepted = !priv->l3cfg_notify.wait_dhcp_commit,
+                },
+        };
+
+        _emit_notify(self, &notify_data);
+    }
 }
 
 static void
 daemon_watch_cb(GPid pid, int status, gpointer user_data)
 {
-    NMDhcpClient *       self = NM_DHCP_CLIENT(user_data);
+    NMDhcpClient        *self = NM_DHCP_CLIENT(user_data);
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    gs_free char *       desc = NULL;
+    gs_free char        *desc = NULL;
 
     g_return_if_fail(priv->watch_id);
     priv->watch_id = 0;
@@ -554,22 +415,7 @@ daemon_watch_cb(GPid pid, int status, gpointer user_data)
 
     priv->pid = -1;
 
-    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL, NULL);
-}
-
-void
-nm_dhcp_client_start_timeout(NMDhcpClient *self)
-{
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
-
-    g_return_if_fail(priv->timeout_id == 0);
-
-    /* Set up a timeout on the transaction to kill it after the timeout */
-
-    if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY)
-        return;
-
-    priv->timeout_id = g_timeout_add_seconds(priv->timeout, transaction_timeout, self);
+    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL);
 }
 
 void
@@ -580,8 +426,6 @@ nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid)
     g_return_if_fail(priv->pid == -1);
     priv->pid = pid;
 
-    nm_dhcp_client_start_timeout(self);
-
     g_return_if_fail(priv->watch_id == 0);
     priv->watch_id = g_child_watch_add(pid, daemon_watch_cb, self);
 }
@@ -595,14 +439,10 @@ nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid)
     priv->pid = -1;
 
     watch_cleanup(self);
-    timeout_cleanup(self);
 }
 
 gboolean
-nm_dhcp_client_start_ip4(NMDhcpClient *self,
-                         GBytes *      client_id,
-                         const char *  last_ip4_address,
-                         GError **     error)
+nm_dhcp_client_start_ip4(NMDhcpClient *self, GError **error)
 {
     NMDhcpClientPrivate *priv;
 
@@ -610,24 +450,25 @@ nm_dhcp_client_start_ip4(NMDhcpClient *self,
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
     g_return_val_if_fail(priv->pid == -1, FALSE);
-    g_return_val_if_fail(priv->addr_family == AF_INET, FALSE);
-    g_return_val_if_fail(priv->uuid != NULL, FALSE);
+    g_return_val_if_fail(priv->config.addr_family == AF_INET, FALSE);
+    g_return_val_if_fail(priv->config.uuid, FALSE);
 
-    if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY)
-        _LOGI("activation: beginning transaction (no timeout)");
-    else
-        _LOGI("activation: beginning transaction (timeout in %u seconds)", (guint) priv->timeout);
-
-    nm_dhcp_client_set_client_id(self, client_id);
+    schedule_no_lease_timeout(self);
 
-    return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, last_ip4_address, error);
+    return NM_DHCP_CLIENT_GET_CLASS(self)->ip4_start(self, error);
 }
 
 gboolean
 nm_dhcp_client_accept(NMDhcpClient *self, GError **error)
 {
+    NMDhcpClientPrivate *priv;
+
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
 
+    priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->l3cd, FALSE);
+
     if (NM_DHCP_CLIENT_GET_CLASS(self)->accept) {
         return NM_DHCP_CLIENT_GET_CLASS(self)->accept(self, error);
     }
@@ -652,8 +493,14 @@ nm_dhcp_client_can_accept(NMDhcpClient *self)
 gboolean
 nm_dhcp_client_decline(NMDhcpClient *self, const char *error_message, GError **error)
 {
+    NMDhcpClientPrivate *priv;
+
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
 
+    priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    g_return_val_if_fail(priv->l3cd, FALSE);
+
     if (NM_DHCP_CLIENT_GET_CLASS(self)->decline) {
         return NM_DHCP_CLIENT_GET_CLASS(self)->decline(self, error_message, error);
     }
@@ -667,43 +514,185 @@ get_duid(NMDhcpClient *self)
     return NULL;
 }
 
+static gboolean
+ipv6_lladdr_timeout(gpointer user_data)
+{
+    NMDhcpClient        *self = user_data;
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
+
+    _emit_notify(
+        self,
+        &((NMDhcpClientNotifyData){
+            .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+            .it_looks_bad.reason = "timeout reached while waiting for an IPv6 link-local address",
+        }));
+    return G_SOURCE_CONTINUE;
+}
+
+static const NMPlatformIP6Address *
+ipv6_lladdr_find(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    NML3Cfg             *l3cfg;
+    NMPLookup            lookup;
+    NMDedupMultiIter     iter;
+    const NMPObject     *obj;
+
+    l3cfg = priv->config.l3cfg;
+    nmp_lookup_init_object(&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);
+
+        if (!IN6_IS_ADDR_LINKLOCAL(&pladdr->address))
+            continue;
+        if (NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_TENTATIVE)
+            && !NM_FLAGS_HAS(pladdr->n_ifa_flags, IFA_F_OPTIMISTIC))
+            continue;
+        return pladdr;
+    }
+    return NULL;
+}
+
+static void
+l3_cfg_notify_cb(NML3Cfg *l3cfg, const NML3ConfigNotifyData *notify_data, NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    nm_assert(l3cfg == priv->config.l3cfg);
+
+    switch (notify_data->notify_type) {
+    case NM_L3_CONFIG_NOTIFY_TYPE_PLATFORM_CHANGE_ON_IDLE:
+    {
+        const NMPlatformIP6Address *addr;
+        gs_free_error GError       *error = NULL;
+
+        if (!priv->l3cfg_notify.wait_ll_address)
+            return;
+
+        addr = ipv6_lladdr_find(self);
+        if (addr) {
+            _LOGD("got IPv6LL address, starting transaction");
+            priv->l3cfg_notify.wait_ll_address = FALSE;
+            connect_l3cfg_notify(self);
+            nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
+
+            schedule_no_lease_timeout(self);
+
+            if (!NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, &error)) {
+                _emit_notify(self,
+                             &((NMDhcpClientNotifyData){
+                                 .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                                 .it_looks_bad.reason = error->message,
+                             }));
+            }
+        }
+
+        break;
+    }
+    case NM_L3_CONFIG_NOTIFY_TYPE_POST_COMMIT:
+    {
+        const NML3ConfigData      *committed_l3cd;
+        NMDedupMultiIter           ipconf_iter;
+        const NMPlatformIPAddress *lease_address;
+        gs_free_error GError      *error = NULL;
+
+        /* A new configuration was committed to the interface. If we previously
+         * got a lease, check whether we are waiting for the address to be
+         * configured. If the address was added, we can proceed accepting the
+         * lease and notifying NMDevice. */
+
+        if (!priv->l3cfg_notify.wait_dhcp_commit)
+            return;
+
+        nm_l3_config_data_iter_ip_address_for_each (&ipconf_iter,
+                                                    priv->l3cd,
+                                                    priv->config.addr_family,
+                                                    &lease_address)
+            break;
+        nm_assert(lease_address);
+        committed_l3cd = nm_l3cfg_get_combined_l3cd(l3cfg, TRUE);
+
+        if (priv->config.addr_family == AF_INET) {
+            const NMPlatformIP4Address *address4 = (const NMPlatformIP4Address *) lease_address;
+
+            if (!nm_l3_config_data_lookup_address_4(committed_l3cd,
+                                                    address4->address,
+                                                    address4->plen,
+                                                    address4->peer_address))
+                return;
+        } else {
+            const NMPlatformIP6Address *address6 = (const NMPlatformIP6Address *) lease_address;
+
+            if (!nm_l3_config_data_lookup_address_6(committed_l3cd, &address6->address))
+                return;
+        }
+
+        priv->l3cfg_notify.wait_dhcp_commit = FALSE;
+        connect_l3cfg_notify(self);
+
+        _LOGD("accept address");
+
+        if (!nm_dhcp_client_accept(self, &error)) {
+            gs_free char *reason = g_strdup_printf("error accepting lease: %s", error->message);
+
+            _emit_notify(self,
+                         &((NMDhcpClientNotifyData){
+                             .notify_type         = NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+                             .it_looks_bad.reason = reason,
+                         }));
+            return;
+        }
+
+        _emit_notify(
+            self,
+            &((NMDhcpClientNotifyData){.notify_type  = NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+                                       .lease_update = {
+                                           .l3cd     = priv->l3cd,
+                                           .accepted = TRUE,
+                                       }}));
+        break;
+    };
+    default:
+        /* ignore */;
+    }
+}
+
 gboolean
-nm_dhcp_client_start_ip6(NMDhcpClient *            self,
-                         GBytes *                  client_id,
-                         gboolean                  enforce_duid,
-                         const struct in6_addr *   ll_addr,
-                         NMSettingIP6ConfigPrivacy privacy,
-                         guint                     needed_prefixes,
-                         GError **                 error)
+nm_dhcp_client_start_ip6(NMDhcpClient *self, GError **error)
 {
-    NMDhcpClientPrivate *priv;
-    gs_unref_bytes GBytes *own_client_id = NULL;
+    NMDhcpClientPrivate        *priv;
+    gs_unref_bytes GBytes      *own_client_id = NULL;
+    const NMPlatformIP6Address *addr;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
-    g_return_val_if_fail(client_id, FALSE);
-
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
     g_return_val_if_fail(priv->pid == -1, FALSE);
-    g_return_val_if_fail(priv->addr_family == AF_INET6, FALSE);
-    g_return_val_if_fail(priv->uuid != NULL, FALSE);
-    g_return_val_if_fail(!priv->client_id, FALSE);
+    g_return_val_if_fail(priv->config.addr_family == AF_INET6, FALSE);
+    g_return_val_if_fail(priv->config.uuid, FALSE);
+    g_return_val_if_fail(!priv->effective_client_id, FALSE);
 
-    if (!enforce_duid)
+    if (!priv->config.v6.enforce_duid)
         own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self);
 
-    _set_client_id(self, own_client_id ?: client_id, FALSE);
+    _set_effective_client_id(self, own_client_id ?: priv->config.client_id, FALSE);
 
-    if (priv->timeout == NM_DHCP_TIMEOUT_INFINITY)
-        _LOGI("activation: beginning transaction (no timeout)");
-    else
-        _LOGI("activation: beginning transaction (timeout in %u seconds)", (guint) priv->timeout);
+    addr = ipv6_lladdr_find(self);
+    if (!addr) {
+        _LOGD("waiting for IPv6LL address");
+        priv->l3cfg_notify.wait_ll_address = TRUE;
+        connect_l3cfg_notify(self);
+        priv->ipv6_lladdr_timeout_source =
+            nm_g_timeout_add_seconds_source(10, ipv6_lladdr_timeout, self);
+        return TRUE;
+    }
 
-    return NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self,
-                                                     ll_addr,
-                                                     privacy,
-                                                     needed_prefixes,
-                                                     error);
+    schedule_no_lease_timeout(self);
+
+    return NM_DHCP_CLIENT_GET_CLASS(self)->ip6_start(self, &addr->address, error);
 }
 
 void
@@ -711,7 +700,7 @@ nm_dhcp_client_stop_existing(const char *pid_file, const char *binary_name)
 {
     guint64       start_time;
     pid_t         pid, ppid;
-    const char *  exe;
+    const char   *exe;
     char          proc_path[NM_STRLEN("/proc/%lu/cmdline") + 100];
     gs_free char *pid_contents = NULL, *proc_contents = NULL;
 
@@ -780,6 +769,10 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
 
     priv->is_stopped = TRUE;
 
+    priv->l3cfg_notify.wait_dhcp_commit = FALSE;
+    priv->l3cfg_notify.wait_ll_address  = FALSE;
+    connect_l3cfg_notify(self);
+
     /* Kill the DHCP client */
     old_pid = priv->pid;
     NM_DHCP_CLIENT_GET_CLASS(self)->stop(self, release);
@@ -789,7 +782,7 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
         _LOGI("canceled DHCP transaction");
     nm_assert(priv->pid == -1);
 
-    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL, NULL);
+    nm_dhcp_client_set_state(self, NM_DHCP_STATE_TERMINATED, NULL);
 }
 
 /*****************************************************************************/
@@ -799,10 +792,10 @@ bytearray_variant_to_string(NMDhcpClient *self, GVariant *value, const char *key
 {
     const guint8 *array;
     gsize         length;
-    GString *     str;
+    GString      *str;
     int           i;
     unsigned char c;
-    char *        converted = NULL;
+    char         *converted = NULL;
 
     g_return_val_if_fail(value != NULL, NULL);
 
@@ -879,7 +872,7 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
          */
         if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
             gs_free guint8 *check_val = NULL;
-            char *          hex_str   = NULL;
+            char           *hex_str   = NULL;
             gsize           len;
 
             /* dhclient passes values from dhcp private options in its own "string" format:
@@ -915,18 +908,16 @@ nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient *self, const NMPlatformIP
 
 gboolean
 nm_dhcp_client_handle_event(gpointer      unused,
-                            const char *  iface,
+                            const char   *iface,
                             int           pid,
-                            GVariant *    options,
-                            const char *  reason,
+                            GVariant     *options,
+                            const char   *reason,
                             NMDhcpClient *self)
 {
-    NMDhcpClientPrivate *priv;
-    guint32              old_state;
-    guint32              new_state;
-    gs_unref_hashtable GHashTable *str_options = NULL;
-    gs_unref_object NMIPConfig *ip_config      = NULL;
-    NMPlatformIP6Address        prefix         = {
+    NMDhcpClientPrivate                    *priv;
+    guint32                                 new_state;
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd   = NULL;
+    NMPlatformIP6Address                    prefix = {
         0,
     };
 
@@ -938,25 +929,26 @@ nm_dhcp_client_handle_event(gpointer      unused,
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    if (g_strcmp0(priv->iface, iface) != 0)
+    if (!nm_streq0(priv->config.iface, iface))
         return FALSE;
     if (priv->pid != pid)
         return FALSE;
 
-    old_state = priv->state;
-    new_state = reason_to_state(self, priv->iface, reason);
+    new_state = reason_to_state(self, priv->config.iface, reason);
+    if (new_state == NM_DHCP_STATE_NOOP)
+        return TRUE;
+
     _LOGD("DHCP state '%s' -> '%s' (reason: '%s')",
-          nm_dhcp_state_to_string(old_state),
+          nm_dhcp_state_to_string(priv->state),
           nm_dhcp_state_to_string(new_state),
           reason);
-
-    if (new_state == NM_DHCP_STATE_NOOP)
-        return TRUE;
+    priv->state = new_state;
 
     if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED)) {
-        GVariantIter iter;
-        const char * name;
-        GVariant *   value;
+        gs_unref_hashtable GHashTable *str_options = NULL;
+        GVariantIter                   iter;
+        const char                    *name;
+        GVariant                      *value;
 
         /* Copy options */
         str_options = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
@@ -968,25 +960,29 @@ nm_dhcp_client_handle_event(gpointer      unused,
 
         /* Create the IP config */
         if (g_hash_table_size(str_options) > 0) {
-            if (priv->addr_family == AF_INET) {
-                ip_config = NM_IP_CONFIG_CAST(
-                    nm_dhcp_utils_ip4_config_from_options(nm_dhcp_client_get_multi_idx(self),
-                                                          priv->ifindex,
-                                                          priv->iface,
-                                                          str_options,
-                                                          priv->route_table,
-                                                          priv->route_metric));
+            if (priv->config.addr_family == AF_INET) {
+                l3cd = nm_dhcp_utils_ip4_config_from_options(
+                    nm_l3cfg_get_multi_idx(priv->config.l3cfg),
+                    nm_l3cfg_get_ifindex(priv->config.l3cfg),
+                    priv->config.iface,
+                    str_options);
             } else {
-                prefix    = nm_dhcp_utils_ip6_prefix_from_options(str_options);
-                ip_config = NM_IP_CONFIG_CAST(nm_dhcp_utils_ip6_config_from_options(
-                    nm_dhcp_client_get_multi_idx(self),
-                    priv->ifindex,
-                    priv->iface,
+                prefix = nm_dhcp_utils_ip6_prefix_from_options(str_options);
+                l3cd   = nm_dhcp_utils_ip6_config_from_options(
+                    nm_l3cfg_get_multi_idx(priv->config.l3cfg),
+                    nm_l3cfg_get_ifindex(priv->config.l3cfg),
+                    priv->config.iface,
                     str_options,
-                    NM_FLAGS_HAS(priv->client_flags, NM_DHCP_CLIENT_FLAGS_INFO_ONLY)));
+                    priv->config.v6.info_only);
             }
         } else
             g_warn_if_reached();
+
+        if (l3cd) {
+            nm_l3_config_data_set_dhcp_lease_from_options(l3cd,
+                                                          priv->config.addr_family,
+                                                          g_steal_pointer(&str_options));
+        }
     }
 
     if (!IN6_IS_ADDR_UNSPECIFIED(&prefix.address)) {
@@ -994,17 +990,16 @@ nm_dhcp_client_handle_event(gpointer      unused,
          * of the DHCP client instance. Instead, we just signal the prefix
          * to the device. */
         nm_dhcp_client_emit_ipv6_prefix_delegated(self, &prefix);
-    } else {
-        /* Fail if no valid IP config was received */
-        if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED) && !ip_config) {
-            _LOGW("client bound but IP config not received");
-            new_state = NM_DHCP_STATE_FAIL;
-            nm_clear_pointer(&str_options, g_hash_table_unref);
-        }
+        return TRUE;
+    }
 
-        nm_dhcp_client_set_state(self, new_state, ip_config, str_options);
+    /* Fail if no valid IP config was received */
+    if (NM_IN_SET(new_state, NM_DHCP_STATE_BOUND, NM_DHCP_STATE_EXTENDED) && !l3cd) {
+        _LOGW("client bound but IP config not received");
+        new_state = NM_DHCP_STATE_FAIL;
     }
 
+    nm_dhcp_client_set_state(self, new_state, l3cd);
     return TRUE;
 }
 
@@ -1016,18 +1011,18 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
     guint                i;
 
     /* IPv6 not implemented yet */
-    nm_assert(priv->addr_family == AF_INET);
+    nm_assert(priv->config.addr_family == AF_INET);
 
-    if (!priv->reject_servers || !priv->reject_servers[0])
+    if (!priv->config.reject_servers || !priv->config.reject_servers[0])
         return FALSE;
 
-    for (i = 0; priv->reject_servers[i]; i++) {
+    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->reject_servers[i],
+                                              priv->config.reject_servers[i],
                                               NULL,
                                               &r_addr,
                                               &r_prefix))
@@ -1040,146 +1035,148 @@ nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr)
     return FALSE;
 }
 
-/*****************************************************************************/
+static void
+config_init(NMDhcpClientConfig *config, const NMDhcpClientConfig *src)
+{
+    *config = *src;
+
+    g_object_ref(config->l3cfg);
+
+    if (config->hwaddr)
+        g_bytes_ref(config->hwaddr);
+    if (config->bcast_hwaddr)
+        g_bytes_ref(config->bcast_hwaddr);
+    if (config->vendor_class_identifier)
+        g_bytes_ref(config->vendor_class_identifier);
+    if (config->client_id)
+        g_bytes_ref(config->client_id);
+
+    config->iface           = g_strdup(config->iface);
+    config->uuid            = g_strdup(config->uuid);
+    config->anycast_address = g_strdup(config->anycast_address);
+    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);
+
+    if (config->addr_family == AF_INET) {
+        config->v4.last_address = g_strdup(config->v4.last_address);
+    } else if (config->addr_family == AF_INET6) {
+        config->hwaddr       = NULL;
+        config->bcast_hwaddr = NULL;
+        config->use_fqdn     = TRUE;
+    } else {
+        nm_assert_not_reached();
+    }
+
+    if (!config->hostname && config->send_hostname) {
+        const char   *hostname;
+        gs_free char *hostname_tmp = NULL;
+
+        hostname = nm_hostname_manager_get_static_hostname(nm_hostname_manager_get());
+
+        if (nm_utils_is_specific_hostname(hostname)) {
+            if (config->addr_family == AF_INET) {
+                char *dot;
+
+                hostname_tmp = g_strdup(hostname);
+                dot          = strchr(hostname_tmp, '.');
+                if (dot)
+                    *dot = '\0';
+            }
+            config->hostname = hostname_tmp ? g_steal_pointer(&hostname_tmp) : g_strdup(hostname);
+        }
+    }
+
+    if (config->hostname) {
+        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))) {
+            nm_log_warn(LOGD_DHCP,
+                        "dhcp%c: %s '%s' is invalid, will be ignored",
+                        nm_utils_addr_family_to_char(config->addr_family),
+                        config->use_fqdn ? "FQDN" : "hostname",
+                        config->hostname);
+            nm_clear_g_free((gpointer *) &config->hostname);
+        }
+    }
+}
 
 static void
-get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+config_clear(NMDhcpClientConfig *config)
 {
-    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(object);
+    g_object_unref(config->l3cfg);
 
-    switch (prop_id) {
-    case PROP_IFACE:
-        g_value_set_string(value, priv->iface);
-        break;
-    case PROP_IFINDEX:
-        g_value_set_int(value, priv->ifindex);
-        break;
-    case PROP_HWADDR:
-        g_value_set_boxed(value, priv->hwaddr);
-        break;
-    case PROP_BROADCAST_HWADDR:
-        g_value_set_boxed(value, priv->bcast_hwaddr);
-        break;
-    case PROP_ADDR_FAMILY:
-        g_value_set_int(value, priv->addr_family);
-        break;
-    case PROP_UUID:
-        g_value_set_string(value, priv->uuid);
-        break;
-    case PROP_IAID:
-        g_value_set_uint(value, priv->iaid);
-        break;
-    case PROP_IAID_EXPLICIT:
-        g_value_set_boolean(value, priv->iaid_explicit);
-        break;
-    case PROP_HOSTNAME:
-        g_value_set_string(value, priv->hostname);
-        break;
-    case PROP_ROUTE_METRIC:
-        g_value_set_uint(value, priv->route_metric);
-        break;
-    case PROP_ROUTE_TABLE:
-        g_value_set_uint(value, priv->route_table);
-        break;
-    case PROP_TIMEOUT:
-        g_value_set_uint(value, priv->timeout);
-        break;
-    default:
-        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
-        break;
+    nm_clear_pointer(&config->hwaddr, g_bytes_unref);
+    nm_clear_pointer(&config->bcast_hwaddr, g_bytes_unref);
+    nm_clear_pointer(&config->vendor_class_identifier, g_bytes_unref);
+    nm_clear_pointer(&config->client_id, g_bytes_unref);
+
+    nm_clear_g_free((gpointer *) &config->iface);
+    nm_clear_g_free((gpointer *) &config->uuid);
+    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);
+
+    if (config->addr_family == AF_INET) {
+        nm_clear_g_free((gpointer *) &config->v4.last_address);
     }
 }
 
+int
+nm_dhcp_client_get_addr_family(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->config.addr_family;
+}
+
+const char *
+nm_dhcp_client_get_iface(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->config.iface;
+}
+
+NMDedupMultiIndex *
+nm_dhcp_client_get_multi_idx(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return nm_l3cfg_get_multi_idx(priv->config.l3cfg);
+}
+
+int
+nm_dhcp_client_get_ifindex(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return nm_l3cfg_get_ifindex(priv->config.l3cfg);
+}
+
+GBytes *
+nm_dhcp_client_get_effective_client_id(NMDhcpClient *self)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+
+    return priv->effective_client_id;
+}
+
+/*****************************************************************************/
+
 static void
 set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
 {
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(object);
-    guint                flags;
 
     switch (prop_id) {
-    case PROP_FLAGS:
-        /* construct-only */
-        flags = g_value_get_uint(value);
-        nm_assert(!NM_FLAGS_ANY(flags, ~((guint) NM_DHCP_CLIENT_FLAGS_ALL)));
-        priv->client_flags = flags;
-        break;
-    case PROP_MULTI_IDX:
-        /* construct-only */
-        priv->multi_idx = g_value_get_pointer(value);
-        if (!priv->multi_idx)
-            g_return_if_reached();
-        nm_dedup_multi_index_ref(priv->multi_idx);
-        break;
-    case PROP_IFACE:
-        /* construct-only */
-        priv->iface = g_value_dup_string(value);
-        g_return_if_fail(priv->iface);
-        nm_assert(nm_utils_ifname_valid_kernel(priv->iface, NULL));
-        break;
-    case PROP_IFINDEX:
-        /* construct-only */
-        priv->ifindex = g_value_get_int(value);
-        g_return_if_fail(priv->ifindex > 0);
-        break;
-    case PROP_HWADDR:
-        /* construct-only */
-        priv->hwaddr = g_value_dup_boxed(value);
-        break;
-    case PROP_ANYCAST_ADDRESS:
-        /* construct-only */
-        priv->anycast_address = g_value_dup_string(value);
-        break;
-    case PROP_BROADCAST_HWADDR:
-        /* construct-only */
-        priv->bcast_hwaddr = g_value_dup_boxed(value);
-        break;
-    case PROP_ADDR_FAMILY:
-        /* construct-only */
-        priv->addr_family = g_value_get_int(value);
-        if (!NM_IN_SET(priv->addr_family, AF_INET, AF_INET6))
-            g_return_if_reached();
-        break;
-    case PROP_UUID:
-        /* construct-only */
-        priv->uuid = g_value_dup_string(value);
-        break;
-    case PROP_IAID:
-        /* construct-only */
-        priv->iaid = g_value_get_uint(value);
-        break;
-    case PROP_IAID_EXPLICIT:
-        /* construct-only */
-        priv->iaid_explicit = g_value_get_boolean(value);
-        break;
-    case PROP_HOSTNAME:
-        /* construct-only */
-        priv->hostname = g_value_dup_string(value);
-        break;
-    case PROP_HOSTNAME_FLAGS:
+    case PROP_CONFIG:
         /* construct-only */
-        priv->hostname_flags = g_value_get_uint(value);
-        break;
-    case PROP_MUD_URL:
-        /* construct-only */
-        priv->mud_url = g_value_dup_string(value);
-        break;
-    case PROP_ROUTE_TABLE:
-        priv->route_table = g_value_get_uint(value);
-        break;
-    case PROP_ROUTE_METRIC:
-        priv->route_metric = g_value_get_uint(value);
-        break;
-    case PROP_TIMEOUT:
-        /* construct-only */
-        priv->timeout = g_value_get_uint(value);
-        break;
-    case PROP_VENDOR_CLASS_IDENTIFIER:
-        /* construct-only */
-        priv->vendor_class_identifier = g_value_dup_boxed(value);
-        break;
-    case PROP_REJECT_SERVERS:
-        /* construct-only */
-        priv->reject_servers = nm_strv_dup_packed(g_value_get_boxed(value), -1);
+        config_init(&priv->config, g_value_get_pointer(value));
         break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
@@ -1200,53 +1197,31 @@ nm_dhcp_client_init(NMDhcpClient *self)
     priv->pid = -1;
 }
 
-#if NM_MORE_ASSERTS
 static void
-constructed(GObject *object)
+dispose(GObject *object)
 {
-    NMDhcpClient *       self = NM_DHCP_CLIENT(object);
+    NMDhcpClient        *self = NM_DHCP_CLIENT(object);
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    /* certain flags only make sense with certain address family. Assert
-     * for that. */
-    if (NM_IS_IPv4(priv->addr_family))
-        nm_assert(!NM_FLAGS_ANY(priv->client_flags, NM_DHCP_CLIENT_FLAGS_INFO_ONLY));
-    else {
-        nm_assert(NM_FLAGS_HAS(priv->client_flags, NM_DHCP_CLIENT_FLAGS_USE_FQDN));
-        nm_assert(!NM_FLAGS_ANY(priv->client_flags, NM_DHCP_CLIENT_FLAGS_REQUEST_BROADCAST));
-    }
+    nm_dhcp_client_stop(self, FALSE);
 
-    nm_assert(!priv->anycast_address || nm_utils_hwaddr_valid(priv->anycast_address, ETH_ALEN));
+    watch_cleanup(self);
+
+    nm_clear_g_source_inst(&priv->no_lease_timeout_source);
+    nm_clear_g_source_inst(&priv->ipv6_lladdr_timeout_source);
 
-    G_OBJECT_CLASS(nm_dhcp_client_parent_class)->constructed(object);
+    G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object);
 }
-#endif
 
 static void
-dispose(GObject *object)
+finalize(GObject *object)
 {
-    NMDhcpClient *       self = NM_DHCP_CLIENT(object);
+    NMDhcpClient        *self = NM_DHCP_CLIENT(object);
     NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
-    nm_dhcp_client_stop(self, FALSE);
-
-    watch_cleanup(self);
-    timeout_cleanup(self);
-
-    nm_clear_g_free(&priv->iface);
-    nm_clear_g_free(&priv->hostname);
-    nm_clear_g_free(&priv->uuid);
-    nm_clear_g_free(&priv->anycast_address);
-    nm_clear_g_free(&priv->mud_url);
-    nm_clear_g_free(&priv->reject_servers);
-    nm_clear_pointer(&priv->client_id, g_bytes_unref);
-    nm_clear_pointer(&priv->hwaddr, g_bytes_unref);
-    nm_clear_pointer(&priv->bcast_hwaddr, g_bytes_unref);
-    nm_clear_pointer(&priv->vendor_class_identifier, g_bytes_unref);
-
-    G_OBJECT_CLASS(nm_dhcp_client_parent_class)->dispose(object);
+    config_clear(&priv->config);
 
-    priv->multi_idx = nm_dedup_multi_index_unref(priv->multi_idx);
+    G_OBJECT_CLASS(nm_dhcp_client_parent_class)->finalize(object);
 }
 
 static void
@@ -1256,165 +1231,19 @@ nm_dhcp_client_class_init(NMDhcpClientClass *client_class)
 
     g_type_class_add_private(client_class, sizeof(NMDhcpClientPrivate));
 
-#if NM_MORE_ASSERTS
-    object_class->constructed = constructed;
-#endif
     object_class->dispose      = dispose;
-    object_class->get_property = get_property;
+    object_class->finalize     = finalize;
     object_class->set_property = set_property;
 
     client_class->stop     = stop;
     client_class->get_duid = get_duid;
 
-    obj_properties[PROP_MULTI_IDX] =
-        g_param_spec_pointer(NM_DHCP_CLIENT_MULTI_IDX,
+    obj_properties[PROP_CONFIG] =
+        g_param_spec_pointer(NM_DHCP_CLIENT_CONFIG,
                              "",
                              "",
                              G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
-    obj_properties[PROP_IFACE] =
-        g_param_spec_string(NM_DHCP_CLIENT_INTERFACE,
-                            "",
-                            "",
-                            NULL,
-                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_IFINDEX] =
-        g_param_spec_int(NM_DHCP_CLIENT_IFINDEX,
-                         "",
-                         "",
-                         -1,
-                         G_MAXINT,
-                         -1,
-                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_HWADDR] =
-        g_param_spec_boxed(NM_DHCP_CLIENT_HWADDR,
-                           "",
-                           "",
-                           G_TYPE_BYTES,
-                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_BROADCAST_HWADDR] =
-        g_param_spec_boxed(NM_DHCP_CLIENT_BROADCAST_HWADDR,
-                           "",
-                           "",
-                           G_TYPE_BYTES,
-                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_ADDR_FAMILY] =
-        g_param_spec_int(NM_DHCP_CLIENT_ADDR_FAMILY,
-                         "",
-                         "",
-                         0,
-                         G_MAXINT,
-                         AF_UNSPEC,
-                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_ANYCAST_ADDRESS] =
-        g_param_spec_string(NM_DHCP_CLIENT_ANYCAST_ADDRESS,
-                            "",
-                            "",
-                            NULL,
-                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_UUID] =
-        g_param_spec_string(NM_DHCP_CLIENT_UUID,
-                            "",
-                            "",
-                            NULL,
-                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_IAID] =
-        g_param_spec_uint(NM_DHCP_CLIENT_IAID,
-                          "",
-                          "",
-                          0,
-                          G_MAXUINT32,
-                          0,
-                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_IAID_EXPLICIT] =
-        g_param_spec_boolean(NM_DHCP_CLIENT_IAID_EXPLICIT,
-                             "",
-                             "",
-                             FALSE,
-                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_HOSTNAME] =
-        g_param_spec_string(NM_DHCP_CLIENT_HOSTNAME,
-                            "",
-                            "",
-                            NULL,
-                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_HOSTNAME_FLAGS] =
-        g_param_spec_uint(NM_DHCP_CLIENT_HOSTNAME_FLAGS,
-                          "",
-                          "",
-                          0,
-                          G_MAXUINT32,
-                          NM_DHCP_HOSTNAME_FLAG_NONE,
-                          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_MUD_URL] =
-        g_param_spec_string(NM_DHCP_CLIENT_MUD_URL,
-                            "",
-                            "",
-                            NULL,
-                            G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_ROUTE_TABLE] =
-        g_param_spec_uint(NM_DHCP_CLIENT_ROUTE_TABLE,
-                          "",
-                          "",
-                          0,
-                          G_MAXUINT32,
-                          RT_TABLE_MAIN,
-                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_ROUTE_METRIC] =
-        g_param_spec_uint(NM_DHCP_CLIENT_ROUTE_METRIC,
-                          "",
-                          "",
-                          0,
-                          G_MAXUINT32,
-                          0,
-                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
-
-    G_STATIC_ASSERT_EXPR(G_MAXINT32 == NM_DHCP_TIMEOUT_INFINITY);
-    obj_properties[PROP_TIMEOUT] =
-        g_param_spec_uint(NM_DHCP_CLIENT_TIMEOUT,
-                          "",
-                          "",
-                          1,
-                          G_MAXINT32,
-                          NM_DHCP_TIMEOUT_DEFAULT,
-                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_FLAGS] =
-        g_param_spec_uint(NM_DHCP_CLIENT_FLAGS,
-                          "",
-                          "",
-                          0,
-                          G_MAXUINT32,
-                          0,
-                          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_VENDOR_CLASS_IDENTIFIER] =
-        g_param_spec_boxed(NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER,
-                           "",
-                           "",
-                           G_TYPE_BYTES,
-                           G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
-    obj_properties[PROP_REJECT_SERVERS] =
-        g_param_spec_boxed(NM_DHCP_CLIENT_REJECT_SERVERS,
-                           "",
-                           "",
-                           G_TYPE_STRV,
-                           G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
     g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
 
     signals[SIGNAL_NOTIFY] =
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index 2e7e0216..249bd013 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -8,8 +8,6 @@
 
 #include "nm-setting-ip4-config.h"
 #include "nm-setting-ip6-config.h"
-#include "nm-ip4-config.h"
-#include "nm-ip6-config.h"
 #include "nm-dhcp-utils.h"
 
 #define NM_DHCP_TIMEOUT_DEFAULT  ((guint32) 45) /* default DHCP timeout, in seconds */
@@ -24,25 +22,7 @@
 #define NM_DHCP_CLIENT_GET_CLASS(obj) \
     (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_DHCP_CLIENT, NMDhcpClientClass))
 
-#define NM_DHCP_CLIENT_ADDR_FAMILY             "addr-family"
-#define NM_DHCP_CLIENT_ANYCAST_ADDRESS         "anycast-address"
-#define NM_DHCP_CLIENT_FLAGS                   "flags"
-#define NM_DHCP_CLIENT_HWADDR                  "hwaddr"
-#define NM_DHCP_CLIENT_BROADCAST_HWADDR        "broadcast-hwaddr"
-#define NM_DHCP_CLIENT_IFINDEX                 "ifindex"
-#define NM_DHCP_CLIENT_INTERFACE               "iface"
-#define NM_DHCP_CLIENT_MULTI_IDX               "multi-idx"
-#define NM_DHCP_CLIENT_HOSTNAME                "hostname"
-#define NM_DHCP_CLIENT_MUD_URL                 "mud-url"
-#define NM_DHCP_CLIENT_ROUTE_METRIC            "route-metric"
-#define NM_DHCP_CLIENT_ROUTE_TABLE             "route-table"
-#define NM_DHCP_CLIENT_TIMEOUT                 "timeout"
-#define NM_DHCP_CLIENT_UUID                    "uuid"
-#define NM_DHCP_CLIENT_IAID                    "iaid"
-#define NM_DHCP_CLIENT_IAID_EXPLICIT           "iaid-explicit"
-#define NM_DHCP_CLIENT_HOSTNAME_FLAGS          "hostname-flags"
-#define NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER "vendor-class-identifier"
-#define NM_DHCP_CLIENT_REJECT_SERVERS          "reject-servers"
+#define NM_DHCP_CLIENT_CONFIG "config"
 
 #define NM_DHCP_CLIENT_NOTIFY "dhcp-notify"
 
@@ -59,7 +39,27 @@ typedef enum {
 } NMDhcpState;
 
 typedef enum _nm_packed {
-    NM_DHCP_CLIENT_NOTIFY_TYPE_STATE_CHANGED,
+    NM_DHCP_CLIENT_NOTIFY_TYPE_LEASE_UPDATE,
+
+    /* When NM_DHCP_CLIENT_NO_LEASE_TIMEOUT expired and the state
+     * switched from NM_DHCP_CLIENT_STATE_NO_LEASE to
+     * NM_DHCP_CLIENT_STATE_NO_LEASE_WITH_TIMEOUT. */
+    NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT,
+
+    /* NMDhcpClient will indefinitely try to get/renew the lease.
+     * As such, it's never officially in a non-recoverable state.
+     * However, there are cases when it really looks like we won't
+     * be able to get a lease. For example, if the underlying interface
+     * is layer 3 only, if we have no IPv6 link local address for a prolonged
+     * time, or if dhclient is not installed.
+     * But even these cases are potentially recoverable. This is only
+     * a hint to the user (which they might ignore).
+     *
+     * In particular, NM_DHCP_CLIENT_NOTIFY_TYPE_NO_LEASE_TIMEOUT might mean
+     * that the DHCP is currently not running, but that could change
+     * at any moment and from client's side, it does not look bad. */
+    NM_DHCP_CLIENT_NOTIFY_TYPE_IT_LOOKS_BAD,
+
     NM_DHCP_CLIENT_NOTIFY_TYPE_PREFIX_DELEGATED,
 } NMDhcpClientNotifyType;
 
@@ -67,18 +67,124 @@ typedef struct {
     NMDhcpClientNotifyType notify_type;
     union {
         struct {
-            NMIPConfig *ip_config;
-            GHashTable *options;
-            NMDhcpState dhcp_state;
-        } state_changed;
+            /* This is either the new lease information we just received,
+             * or NULL (if a previous lease timed out). It can also be the
+             * previous lease, that was injected. */
+            const NML3ConfigData *l3cd;
+            bool                  accepted;
+        } lease_update;
         struct {
             const NMPlatformIP6Address *prefix;
         } prefix_delegated;
+        struct {
+            const char *reason;
+        } it_looks_bad;
     };
 } NMDhcpClientNotifyData;
 
 const char *nm_dhcp_state_to_string(NMDhcpState state);
 
+/* FIXME(l3cfg:dhcp:config): nm_dhcp_manager_start_ip[46]() has a gazillion of parameters,
+ * those get passed on as CONSTRUCT_ONLY properties to the NMDhcpClient. Drop
+ * all these parameters, and let the caller provide one NMDhcpClientConfig
+ * instance. There will be only one GObject property (NM_DHCP_CLIENT_CONFIG),
+ * which is CONSTRUCT_ONLY and takes a (mandatory) G_TYPE_POINTER for the
+ * configuration.
+ *
+ * Since NMDhcpClientConfig has an addr_family, we also don't need separate
+ * nm_dhcp_manager_start_ip[46]() methods. */
+typedef struct {
+    int addr_family;
+
+    /* The NML3Cfg instance is the manager object for the ifindex on which
+     * NMDhcpClient is supposed to run. */
+    NML3Cfg *l3cfg;
+
+    /* FIXME(l3cfg:dhcp:previous-lease): most parameters of NMDhcpClient are immutable,
+     * so to change them (during reapply), we need to create and start
+     * a new NMDhcpClient instance.
+     *
+     * However, while the restart happens, we want to stick to the previous
+     * lease (if any). Allow the caller to provide such a previous lease,
+     * and if present, the instance starts by pretending that it just received
+     * this lease, before really starting. */
+    const NML3ConfigData *previous_lease;
+
+    const char *iface;
+
+    /* The hardware address */
+    GBytes *hwaddr;
+
+    /* The broadcast hardware address */
+    GBytes *bcast_hwaddr;
+
+    /* Timeout in seconds before reporting failure */
+    guint32 timeout;
+
+    /* Flags for the hostname and FQDN DHCP options */
+    NMDhcpHostnameFlags hostname_flags;
+
+    /* The UUID of the connection. Used mainly to build
+     * lease file names. */
+    const char *uuid;
+
+    /* Set to reduce the number of broadcast packets when the
+	 * anycast hardware address of the DHCP service is known. */
+    const char *anycast_address;
+
+    /* The hostname or FQDN to send. */
+    const char *hostname;
+
+    /* The Manufacturer Usage Description (RFC 8520) URL to send */
+    const char *mud_url;
+
+    /* The value for the Vendor Class Identifier option */
+    GBytes *vendor_class_identifier;
+
+    /* A list of servers from which offers should be rejected */
+    const char *const *reject_servers;
+
+    /* The client identifier (DHCPv4) or DUID (DHCPv6) to send */
+    GBytes *client_id;
+
+    /* Whether to send the hostname or FQDN option */
+    bool send_hostname : 1;
+
+    /* Whether to send the hostname as HOSTNAME option or FQDN.
+     * For DHCPv6 this is always TRUE. */
+    bool use_fqdn : 1;
+
+    union {
+        struct {
+            /* Set BOOTP broadcast flag in request packets, so that servers
+             * will always broadcast replies. */
+            bool request_broadcast : 1;
+
+            /* The address from the previous lease */
+            const char *last_address;
+        } v4;
+        struct {
+            /* If set, the DUID from the connection is used; otherwise
+             * the one from an existing lease is used. */
+            gboolean enforce_duid;
+
+            /* The IAID to use */
+            guint32 iaid;
+
+            /* Whether the IAID was explicitly set in the connection or
+             * as global default */
+            gboolean iaid_explicit;
+
+            /* Number to prefixes (IA_PD) to request */
+            guint needed_prefixes;
+
+            /* Use Information-request to get stateless configuration
+             * parameters (don't request a IA_NA) */
+            bool info_only : 1;
+        } v6;
+    };
+} NMDhcpClientConfig;
+
 struct _NMDhcpClientPrivate;
 
 typedef struct {
@@ -100,17 +206,13 @@ typedef enum _nm_packed {
 typedef struct {
     GObjectClass parent;
 
-    gboolean (*ip4_start)(NMDhcpClient *self, const char *last_ip4_address, GError **error);
+    gboolean (*ip4_start)(NMDhcpClient *self, GError **error);
 
     gboolean (*accept)(NMDhcpClient *self, GError **error);
 
     gboolean (*decline)(NMDhcpClient *self, const char *error_message, GError **error);
 
-    gboolean (*ip6_start)(NMDhcpClient *            self,
-                          const struct in6_addr *   ll_addr,
-                          NMSettingIP6ConfigPrivacy privacy,
-                          guint                     needed_prefixes,
-                          GError **                 error);
+    gboolean (*ip6_start)(NMDhcpClient *self, const struct in6_addr *ll_addr, GError **error);
 
     void (*stop)(NMDhcpClient *self, gboolean release);
 
@@ -128,64 +230,25 @@ typedef struct {
 
 GType nm_dhcp_client_get_type(void);
 
-struct _NMDedupMultiIndex *nm_dhcp_client_get_multi_idx(NMDhcpClient *self);
-
-pid_t nm_dhcp_client_get_pid(NMDhcpClient *self);
-
-int nm_dhcp_client_get_addr_family(NMDhcpClient *self);
-
-const char *nm_dhcp_client_get_iface(NMDhcpClient *self);
-
-int nm_dhcp_client_get_ifindex(NMDhcpClient *self);
-
-const char *nm_dhcp_client_get_uuid(NMDhcpClient *self);
-
-GBytes *nm_dhcp_client_get_duid(NMDhcpClient *self);
-
-GBytes *nm_dhcp_client_get_hw_addr(NMDhcpClient *self);
-
-GBytes *nm_dhcp_client_get_broadcast_hw_addr(NMDhcpClient *self);
-
-const char *nm_dhcp_client_get_anycast_address(NMDhcpClient *self);
-
-guint32 nm_dhcp_client_get_route_table(NMDhcpClient *self);
-
-void nm_dhcp_client_set_route_table(NMDhcpClient *self, guint32 route_table);
+gboolean nm_dhcp_client_start_ip4(NMDhcpClient *self, GError **error);
+gboolean nm_dhcp_client_start_ip6(NMDhcpClient *self, GError **error);
 
-guint32 nm_dhcp_client_get_route_metric(NMDhcpClient *self);
+const NMDhcpClientConfig *nm_dhcp_client_get_config(NMDhcpClient *self);
 
-void nm_dhcp_client_set_route_metric(NMDhcpClient *self, guint32 route_metric);
-
-guint32 nm_dhcp_client_get_timeout(NMDhcpClient *self);
-
-guint32 nm_dhcp_client_get_iaid(NMDhcpClient *self);
-
-gboolean nm_dhcp_client_get_iaid_explicit(NMDhcpClient *self);
-
-GBytes *nm_dhcp_client_get_client_id(NMDhcpClient *self);
-
-const char *       nm_dhcp_client_get_hostname(NMDhcpClient *self);
-const char *       nm_dhcp_client_get_mud_url(NMDhcpClient *self);
-const char *const *nm_dhcp_client_get_reject_servers(NMDhcpClient *self);
-
-NMDhcpHostnameFlags nm_dhcp_client_get_hostname_flags(NMDhcpClient *self);
-
-NMDhcpClientFlags nm_dhcp_client_get_client_flags(NMDhcpClient *self);
-
-GBytes *nm_dhcp_client_get_vendor_class_identifier(NMDhcpClient *self);
-
-gboolean nm_dhcp_client_start_ip4(NMDhcpClient *self,
-                                  GBytes *      client_id,
-                                  const char *  last_ip4_address,
-                                  GError **     error);
+pid_t nm_dhcp_client_get_pid(NMDhcpClient *self);
 
-gboolean nm_dhcp_client_start_ip6(NMDhcpClient *            self,
-                                  GBytes *                  client_id,
-                                  gboolean                  enforce_duid,
-                                  const struct in6_addr *   ll_addr,
-                                  NMSettingIP6ConfigPrivacy privacy,
-                                  guint                     needed_prefixes,
-                                  GError **                 error);
+static inline const NML3ConfigData *
+nm_dhcp_client_get_lease(NMDhcpClient *self)
+{
+    /* FIXME(l3cfg:dhcp:previous-lease): this function returns the currently
+     * valid, exposed lease.
+     *
+     * Note that NMDhcpClient should accept as construct argument a *previous* lease,
+     * and (if that lease is still valid), pretend that it's good to use. The point is
+     * so that during reapply we keep using the current address, until a new lease
+     * was received. */
+    return NULL;
+}
 
 gboolean nm_dhcp_client_accept(NMDhcpClient *self, GError **error);
 gboolean nm_dhcp_client_can_accept(NMDhcpClient *self);
@@ -205,29 +268,29 @@ void nm_dhcp_client_watch_child(NMDhcpClient *self, pid_t pid);
 
 void nm_dhcp_client_stop_watch_child(NMDhcpClient *self, pid_t pid);
 
-void nm_dhcp_client_set_state(NMDhcpClient *self,
-                              NMDhcpState   new_state,
-                              NMIPConfig *  ip_config,
-                              GHashTable *  options); /* str:str hash */
+void
+nm_dhcp_client_set_state(NMDhcpClient *self, NMDhcpState new_state, const NML3ConfigData *l3cd);
 
 gboolean nm_dhcp_client_handle_event(gpointer      unused,
-                                     const char *  iface,
+                                     const char   *iface,
                                      int           pid,
-                                     GVariant *    options,
-                                     const char *  reason,
+                                     GVariant     *options,
+                                     const char   *reason,
                                      NMDhcpClient *self);
 
-void nm_dhcp_client_set_client_id(NMDhcpClient *self, GBytes *client_id);
-void nm_dhcp_client_set_client_id_bin(NMDhcpClient *self,
-                                      guint8        type,
-                                      const guint8 *client_id,
-                                      gsize         len);
-
-void nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient *              self,
+void nm_dhcp_client_emit_ipv6_prefix_delegated(NMDhcpClient               *self,
                                                const NMPlatformIP6Address *prefix);
 
 gboolean nm_dhcp_client_server_id_is_rejected(NMDhcpClient *self, gconstpointer addr);
 
+int                nm_dhcp_client_get_addr_family(NMDhcpClient *self);
+const char        *nm_dhcp_client_get_iface(NMDhcpClient *self);
+NMDedupMultiIndex *nm_dhcp_client_get_multi_idx(NMDhcpClient *self);
+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);
+
 /*****************************************************************************
  * Client data
  *****************************************************************************/
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.c b/src/core/dhcp/nm-dhcp-dhclient-utils.c
index 341ac7b2..74e6b905 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.c
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.c
@@ -15,7 +15,6 @@
 #include "libnm-glib-aux/nm-dedup-multi.h"
 
 #include "nm-dhcp-utils.h"
-#include "nm-ip4-config.h"
 #include "nm-utils.h"
 #include "libnm-platform/nm-platform.h"
 #include "NetworkManagerUtils.h"
@@ -98,9 +97,9 @@ grab_request_options(GPtrArray *store, const char *line)
 }
 
 static void
-add_ip4_config(GString *           str,
-               GBytes *            client_id,
-               const char *        hostname,
+add_ip4_config(GString            *str,
+               GBytes             *client_id,
+               const char         *hostname,
                gboolean            use_fqdn,
                NMDhcpHostnameFlags hostname_flags)
 {
@@ -205,10 +204,12 @@ static GBytes *
 read_client_id(const char *str)
 {
     gs_free char *s = NULL;
-    char *        p;
-    int           i = 0, j = 0;
+    char         *p;
+    int           i = 0;
+    int           j = 0;
+    gsize         l;
 
-    nm_assert(!strncmp(str, CLIENTID_TAG, NM_STRLEN(CLIENTID_TAG)));
+    nm_assert(NM_STR_HAS_PREFIX(str, CLIENTID_TAG));
     str += NM_STRLEN(CLIENTID_TAG);
 
     if (!g_ascii_isspace(*str))
@@ -249,8 +250,9 @@ read_client_id(const char *str)
     /* Otherwise, try to read a hexadecimal sequence */
     s = g_strdup(str);
     g_strchomp(s);
-    if (s[strlen(s) - 1] == ';')
-        s[strlen(s) - 1] = '\0';
+    l = strlen(s);
+    if (l > 0 && s[l - 1] == ';')
+        s[l - 1] = '\0';
 
     return nm_utils_hexstr2bin(s);
 }
@@ -259,7 +261,7 @@ static gboolean
 read_interface(const char *line, char *interface, guint size)
 {
     gs_free char *dup = g_strdup(line + NM_STRLEN("interface"));
-    char *        ptr = dup, *end;
+    char         *ptr = dup, *end;
 
     while (g_ascii_isspace(*ptr))
         ptr++;
@@ -288,25 +290,25 @@ read_interface(const char *line, char *interface, guint size)
 }
 
 char *
-nm_dhcp_dhclient_create_config(const char *        interface,
+nm_dhcp_dhclient_create_config(const char         *interface,
                                int                 addr_family,
-                               GBytes *            client_id,
-                               const char *        anycast_address,
-                               const char *        hostname,
+                               GBytes             *client_id,
+                               const char         *anycast_address,
+                               const char         *hostname,
                                guint32             timeout,
                                gboolean            use_fqdn,
                                NMDhcpHostnameFlags hostname_flags,
-                               const char *        mud_url,
-                               const char *const * reject_servers,
-                               const char *        orig_path,
-                               const char *        orig_contents,
-                               GBytes **           out_new_client_id)
+                               const char         *mud_url,
+                               const char *const  *reject_servers,
+                               const char         *orig_path,
+                               const char         *orig_contents,
+                               GBytes            **out_new_client_id)
 {
-    nm_auto_free_gstring GString *new_contents = NULL;
-    gs_unref_ptrarray GPtrArray *fqdn_opts     = NULL;
-    gs_unref_ptrarray GPtrArray *reqs          = NULL;
-    gboolean                     reset_reqlist = FALSE;
-    int                          i;
+    nm_auto_free_gstring GString *new_contents  = NULL;
+    gs_unref_ptrarray GPtrArray  *fqdn_opts     = NULL;
+    gs_unref_ptrarray GPtrArray  *reqs          = NULL;
+    gboolean                      reset_reqlist = FALSE;
+    int                           i;
 
     g_return_val_if_fail(!anycast_address || nm_utils_hwaddr_valid(anycast_address, ETH_ALEN),
                          NULL);
@@ -318,8 +320,8 @@ nm_dhcp_dhclient_create_config(const char *        interface,
     reqs         = g_ptr_array_new_full(5, g_free);
 
     if (orig_contents) {
-        gs_free const char **lines = NULL;
-        gsize                line_i;
+        gs_free const char          **lines = NULL;
+        gsize                         line_i;
         nm_auto_free_gstring GString *blocks_stack = NULL;
         guint                         blocks_skip  = 0;
         gboolean                      in_alsoreq   = FALSE;
@@ -385,11 +387,10 @@ nm_dhcp_dhclient_create_config(const char *        interface,
              * fail the dhcp process before dhcp-timeout. So, always skip importing timeout
              * as we will need to add one greater than dhcp-timeout.
              */
-            if (!strncmp(p, TIMEOUT_TAG, strlen(TIMEOUT_TAG))
-                || !strncmp(p, RETRY_TAG, strlen(RETRY_TAG)))
+            if (NM_STR_HAS_PREFIX(p, TIMEOUT_TAG) || NM_STR_HAS_PREFIX(p, RETRY_TAG))
                 continue;
 
-            if (!strncmp(p, CLIENTID_TAG, strlen(CLIENTID_TAG))) {
+            if (NM_STR_HAS_PREFIX(p, CLIENTID_TAG)) {
                 /* Override config file "dhcp-client-id" and use one from the connection */
                 if (client_id)
                     continue;
@@ -402,16 +403,16 @@ nm_dhcp_dhclient_create_config(const char *        interface,
 
             /* Override config file hostname and use one from the connection */
             if (hostname) {
-                if (strncmp(p, HOSTNAME4_TAG, strlen(HOSTNAME4_TAG)) == 0)
+                if (NM_STR_HAS_PREFIX(p, HOSTNAME4_TAG))
                     continue;
-                if (strncmp(p, FQDN_TAG, strlen(FQDN_TAG)) == 0)
+                if (NM_STR_HAS_PREFIX(p, FQDN_TAG))
                     continue;
             }
 
             /* To let user's FQDN options (except "fqdn.fqdn") override the
              * default ones set by NM, add them later
              */
-            if (!strncmp(p, FQDN_TAG_PREFIX, NM_STRLEN(FQDN_TAG_PREFIX))) {
+            if (NM_STR_HAS_PREFIX(p, FQDN_TAG_PREFIX)) {
                 if (!fqdn_opts)
                     fqdn_opts = g_ptr_array_new_full(5, g_free);
                 g_ptr_array_add(fqdn_opts, g_strdup(p + NM_STRLEN(FQDN_TAG_PREFIX)));
@@ -423,9 +424,9 @@ nm_dhcp_dhclient_create_config(const char *        interface,
                 continue;
 
             /* Check for "request" */
-            if (!strncmp(p, REQ_TAG, strlen(REQ_TAG))) {
+            if (NM_STR_HAS_PREFIX(p, REQ_TAG)) {
                 in_req = TRUE;
-                p += strlen(REQ_TAG);
+                p += NM_STRLEN(REQ_TAG);
                 g_ptr_array_set_size(reqs, 0);
                 reset_reqlist = TRUE;
             }
@@ -437,9 +438,9 @@ nm_dhcp_dhclient_create_config(const char *        interface,
             }
 
             /* Check for "also require" */
-            if (!strncmp(p, ALSOREQ_TAG, strlen(ALSOREQ_TAG))) {
+            if (NM_STR_HAS_PREFIX(p, ALSOREQ_TAG)) {
                 in_alsoreq = TRUE;
-                p += strlen(ALSOREQ_TAG);
+                p += NM_STRLEN(ALSOREQ_TAG);
             }
 
             if (in_alsoreq) {
@@ -526,10 +527,10 @@ nm_dhcp_dhclient_create_config(const char *        interface,
 char *
 nm_dhcp_dhclient_escape_duid(GBytes *duid)
 {
-    char *        escaped;
+    char         *escaped;
     const guint8 *s, *s0;
     gsize         len;
-    char *        d;
+    char         *d;
 
     g_return_val_if_fail(duid, NULL);
 
@@ -563,7 +564,7 @@ isoctal(const guint8 *p)
 GBytes *
 nm_dhcp_dhclient_unescape_duid(const char *duid)
 {
-    GByteArray *  unescaped;
+    GByteArray   *unescaped;
     const guint8 *p = (const guint8 *) duid;
     guint         i, len;
     guint8        octal;
@@ -610,7 +611,7 @@ error:
 GBytes *
 nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
 {
-    gs_free char *       contents   = NULL;
+    gs_free char        *contents   = NULL;
     gs_free const char **contents_v = NULL;
     gsize                i;
 
@@ -623,7 +624,7 @@ nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
     contents_v = nm_strsplit_set(contents, "\n\r");
     for (i = 0; contents_v && contents_v[i]; i++) {
         const char *p = nm_str_skip_leading_spaces(contents_v[i]);
-        GBytes *    duid;
+        GBytes     *duid;
 
         if (!NM_STR_HAS_PREFIX(p, DUID_PREFIX))
             continue;
@@ -648,10 +649,10 @@ nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
 gboolean
 nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error)
 {
-    gs_free char *       escaped_duid = NULL;
-    gs_free const char **lines        = NULL;
-    nm_auto_free_gstring GString *s   = NULL;
-    const char *const *           iter;
+    gs_free char                 *escaped_duid = NULL;
+    gs_free const char          **lines        = NULL;
+    nm_auto_free_gstring GString *s            = NULL;
+    const char *const            *iter;
     gsize                         len = 0;
 
     g_return_val_if_fail(leasefile != NULL, FALSE);
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.h b/src/core/dhcp/nm-dhcp-dhclient-utils.h
index ed7c1c7c..0ce7dd90 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.h
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.h
@@ -9,19 +9,19 @@
 #include "nm-setting-ip4-config.h"
 #include "nm-setting-ip6-config.h"
 
-char *nm_dhcp_dhclient_create_config(const char *        interface,
+char *nm_dhcp_dhclient_create_config(const char         *interface,
                                      int                 addr_family,
-                                     GBytes *            client_id,
-                                     const char *        anycast_addr,
-                                     const char *        hostname,
+                                     GBytes             *client_id,
+                                     const char         *anycast_addr,
+                                     const char         *hostname,
                                      guint32             timeout,
                                      gboolean            use_fqdn,
                                      NMDhcpHostnameFlags hostname_flags,
-                                     const char *        mud_url,
-                                     const char *const * reject_servers,
-                                     const char *        orig_path,
-                                     const char *        orig_contents,
-                                     GBytes **           out_new_client_id);
+                                     const char         *mud_url,
+                                     const char *const  *reject_servers,
+                                     const char         *orig_path,
+                                     const char         *orig_contents,
+                                     GBytes            **out_new_client_id);
 
 char *nm_dhcp_dhclient_escape_duid(GBytes *duid);
 
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
index 970a51f5..28d40c07 100644
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ b/src/core/dhcp/nm-dhcp-dhclient.c
@@ -59,10 +59,10 @@ static GType nm_dhcp_dhclient_get_type(void);
 /*****************************************************************************/
 
 typedef struct {
-    char *          conf_file;
-    const char *    def_leasefile;
-    char *          lease_file;
-    char *          pid_file;
+    char           *conf_file;
+    const char     *def_leasefile;
+    char           *lease_file;
+    char           *pid_file;
     NMDhcpListener *dhcp_listener;
 } NMDhcpDhclientPrivate;
 
@@ -105,7 +105,7 @@ static char *
 get_dhclient_leasefile(int         addr_family,
                        const char *iface,
                        const char *uuid,
-                       char **     out_preferred_path)
+                       char      **out_preferred_path)
 {
     gs_free char *path = NULL;
 
@@ -145,21 +145,21 @@ get_dhclient_leasefile(int         addr_family,
 }
 
 static gboolean
-merge_dhclient_config(NMDhcpDhclient *    self,
+merge_dhclient_config(NMDhcpDhclient     *self,
                       int                 addr_family,
-                      const char *        iface,
-                      const char *        conf_file,
-                      GBytes *            client_id,
-                      const char *        anycast_address,
-                      const char *        hostname,
+                      const char         *iface,
+                      const char         *conf_file,
+                      GBytes             *client_id,
+                      const char         *anycast_address,
+                      const char         *hostname,
                       guint32             timeout,
                       gboolean            use_fqdn,
                       NMDhcpHostnameFlags hostname_flags,
-                      const char *        mud_url,
-                      const char *const * reject_servers,
-                      const char *        orig_path,
-                      GBytes **           out_new_client_id,
-                      GError **           error)
+                      const char         *mud_url,
+                      const char *const  *reject_servers,
+                      const char         *orig_path,
+                      GBytes            **out_new_client_id,
+                      GError            **error)
 {
     gs_free char *orig = NULL;
     gs_free char *new  = NULL;
@@ -274,19 +274,19 @@ find_existing_config(NMDhcpDhclient *self, int addr_family, const char *iface, c
  * config file along with the NM options.
  */
 static char *
-create_dhclient_config(NMDhcpDhclient *    self,
+create_dhclient_config(NMDhcpDhclient     *self,
                        int                 addr_family,
-                       const char *        iface,
-                       const char *        uuid,
-                       GBytes *            client_id,
-                       const char *        anycast_address,
-                       const char *        hostname,
+                       const char         *iface,
+                       const char         *uuid,
+                       GBytes             *client_id,
+                       const char         *anycast_address,
+                       const char         *hostname,
                        guint32             timeout,
                        gboolean            use_fqdn,
                        NMDhcpHostnameFlags hostname_flags,
-                       const char *        mud_url,
-                       const char *const * reject_servers,
-                       GBytes **           out_new_client_id)
+                       const char         *mud_url,
+                       const char *const  *reject_servers,
+                       GBytes            **out_new_client_id)
 {
     gs_free char *orig = NULL;
     char *new          = NULL;
@@ -330,29 +330,31 @@ create_dhclient_config(NMDhcpDhclient *    self,
 
 static gboolean
 dhclient_start(NMDhcpClient *client,
-               const char *  mode_opt,
+               const char   *mode_opt,
                gboolean      release,
-               pid_t *       out_pid,
-               int           prefixes,
-               GError **     error)
+               pid_t        *out_pid,
+               GError      **error)
 {
-    NMDhcpDhclient *       self       = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate *priv       = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    NMDhcpDhclient              *self = NM_DHCP_DHCLIENT(client);
+    NMDhcpDhclientPrivate       *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
     gs_unref_ptrarray GPtrArray *argv = NULL;
     pid_t                        pid;
-    gs_free_error GError *local = NULL;
-    const char *          iface;
-    const char *          uuid;
-    const char *          system_bus_address;
-    const char *          dhclient_path;
-    char *                binary_name;
-    gs_free char *        cmd_str                  = NULL;
-    gs_free char *        pid_file                 = NULL;
-    gs_free char *        system_bus_address_env   = NULL;
-    gs_free char *        preferred_leasefile_path = NULL;
-    const int             addr_family              = nm_dhcp_client_get_addr_family(client);
+    gs_free_error GError        *local = NULL;
+    const char                  *iface;
+    const char                  *uuid;
+    const char                  *system_bus_address;
+    const char                  *dhclient_path;
+    char                        *binary_name;
+    gs_free char                *cmd_str                  = NULL;
+    gs_free char                *pid_file                 = NULL;
+    gs_free char                *system_bus_address_env   = NULL;
+    gs_free char                *preferred_leasefile_path = NULL;
+    int                          addr_family;
+    const NMDhcpClientConfig    *client_config;
 
     g_return_val_if_fail(!priv->pid_file, FALSE);
+    client_config = nm_dhcp_client_get_config(client);
+    addr_family   = client_config->addr_family;
 
     NM_SET_OUT(out_pid, 0);
 
@@ -362,8 +364,8 @@ dhclient_start(NMDhcpClient *client,
         return FALSE;
     }
 
-    iface = nm_dhcp_client_get_iface(client);
-    uuid  = nm_dhcp_client_get_uuid(client);
+    iface = client_config->iface;
+    uuid  = client_config->uuid;
 
     pid_file = g_strdup_printf(NMRUNDIR "/dhclient%s-%s.pid",
                                _addr_family_to_path_part(addr_family),
@@ -409,9 +411,7 @@ dhclient_start(NMDhcpClient *client,
 
     /* Save the DUID to the leasefile dhclient will actually use */
     if (addr_family == AF_INET6) {
-        if (!nm_dhcp_dhclient_save_duid(priv->lease_file,
-                                        nm_dhcp_client_get_client_id(client),
-                                        &local)) {
+        if (!nm_dhcp_dhclient_save_duid(priv->lease_file, client_config->client_id, &local)) {
             nm_utils_error_set(error,
                                NM_UTILS_ERROR_UNKNOWN,
                                "failed to save DUID to '%s': %s",
@@ -434,13 +434,13 @@ dhclient_start(NMDhcpClient *client,
     if (release)
         g_ptr_array_add(argv, (gpointer) "-r");
 
-    if (!release
-        && NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(NM_DHCP_CLIENT(self)),
-                        NM_DHCP_CLIENT_FLAGS_REQUEST_BROADCAST)) {
+    if (!release && client_config->addr_family == AF_INET && client_config->v4.request_broadcast) {
         g_ptr_array_add(argv, (gpointer) "-B");
     }
 
     if (addr_family == AF_INET6) {
+        guint prefixes = client_config->v6.needed_prefixes;
+
         g_ptr_array_add(argv, (gpointer) "-6");
 
         if (prefixes > 0 && nm_streq0(mode_opt, "-S")) {
@@ -513,29 +513,28 @@ dhclient_start(NMDhcpClient *client,
 }
 
 static gboolean
-ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
+ip4_start(NMDhcpClient *client, GError **error)
 {
-    NMDhcpDhclient *       self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    GBytes *               client_id;
-    gs_unref_bytes GBytes *new_client_id = NULL;
-
-    client_id = nm_dhcp_client_get_client_id(client);
-
-    priv->conf_file = create_dhclient_config(
-        self,
-        AF_INET,
-        nm_dhcp_client_get_iface(client),
-        nm_dhcp_client_get_uuid(client),
-        client_id,
-        nm_dhcp_client_get_anycast_address(client),
-        nm_dhcp_client_get_hostname(client),
-        nm_dhcp_client_get_timeout(client),
-        NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(client), NM_DHCP_CLIENT_FLAGS_USE_FQDN),
-        nm_dhcp_client_get_hostname_flags(client),
-        nm_dhcp_client_get_mud_url(client),
-        nm_dhcp_client_get_reject_servers(client),
-        &new_client_id);
+    NMDhcpDhclient           *self          = NM_DHCP_DHCLIENT(client);
+    NMDhcpDhclientPrivate    *priv          = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    gs_unref_bytes GBytes    *new_client_id = NULL;
+    const NMDhcpClientConfig *client_config;
+
+    client_config = nm_dhcp_client_get_config(client);
+
+    priv->conf_file = create_dhclient_config(self,
+                                             AF_INET,
+                                             client_config->iface,
+                                             client_config->uuid,
+                                             client_config->client_id,
+                                             client_config->anycast_address,
+                                             client_config->hostname,
+                                             client_config->timeout,
+                                             client_config->use_fqdn,
+                                             client_config->hostname_flags,
+                                             client_config->mud_url,
+                                             client_config->reject_servers,
+                                             &new_client_id);
     if (!priv->conf_file) {
         nm_utils_error_set_literal(error,
                                    NM_UTILS_ERROR_UNKNOWN,
@@ -544,36 +543,35 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
     }
 
     if (new_client_id) {
-        nm_assert(!client_id);
-        nm_dhcp_client_set_client_id(client, new_client_id);
+        nm_assert(!client_config->client_id);
+        nm_dhcp_client_set_effective_client_id(client, new_client_id);
     }
-    return dhclient_start(client, NULL, FALSE, NULL, 0, error);
+    return dhclient_start(client, NULL, FALSE, NULL, error);
 }
 
 static gboolean
-ip6_start(NMDhcpClient *            client,
-          const struct in6_addr *   ll_addr,
-          NMSettingIP6ConfigPrivacy privacy,
-          guint                     needed_prefixes,
-          GError **                 error)
+ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
 {
-    NMDhcpDhclient *       self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    NMDhcpDhclient           *self = NM_DHCP_DHCLIENT(client);
+    NMDhcpDhclientPrivate    *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    const NMDhcpClientConfig *config;
 
-    if (nm_dhcp_client_get_iaid_explicit(client))
+    config = nm_dhcp_client_get_config(client);
+
+    if (config->v6.iaid_explicit)
         _LOGW("dhclient does not support specifying an IAID for DHCPv6, it will be ignored");
 
     priv->conf_file = create_dhclient_config(self,
                                              AF_INET6,
-                                             nm_dhcp_client_get_iface(client),
-                                             nm_dhcp_client_get_uuid(client),
+                                             config->iface,
+                                             config->uuid,
                                              NULL,
-                                             nm_dhcp_client_get_anycast_address(client),
-                                             nm_dhcp_client_get_hostname(client),
-                                             nm_dhcp_client_get_timeout(client),
+                                             config->anycast_address,
+                                             config->hostname,
+                                             config->timeout,
                                              TRUE,
-                                             nm_dhcp_client_get_hostname_flags(client),
-                                             nm_dhcp_client_get_mud_url(client),
+                                             config->hostname_flags,
+                                             config->mud_url,
                                              NULL,
                                              NULL);
     if (!priv->conf_file) {
@@ -583,21 +581,13 @@ ip6_start(NMDhcpClient *            client,
         return FALSE;
     }
 
-    return dhclient_start(client,
-                          NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(NM_DHCP_CLIENT(self)),
-                                       NM_DHCP_CLIENT_FLAGS_INFO_ONLY)
-                              ? "-S"
-                              : "-N",
-                          FALSE,
-                          NULL,
-                          needed_prefixes,
-                          error);
+    return dhclient_start(client, config->v6.needed_prefixes ? "-S" : "-N", FALSE, NULL, error);
 }
 
 static void
 stop(NMDhcpClient *client, gboolean release)
 {
-    NMDhcpDhclient *       self = NM_DHCP_DHCLIENT(client);
+    NMDhcpDhclient        *self = NM_DHCP_DHCLIENT(client);
     NMDhcpDhclientPrivate *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
     int                    errsv;
 
@@ -625,7 +615,7 @@ stop(NMDhcpClient *client, gboolean release)
     if (release) {
         pid_t rpid = -1;
 
-        if (dhclient_start(client, NULL, TRUE, &rpid, 0, NULL)) {
+        if (dhclient_start(client, NULL, TRUE, &rpid, NULL)) {
             /* Wait a few seconds for the release to happen */
             nm_dhcp_client_stop_pid(rpid, nm_dhcp_client_get_iface(client));
         }
@@ -635,16 +625,19 @@ stop(NMDhcpClient *client, gboolean release)
 static GBytes *
 get_duid(NMDhcpClient *client)
 {
-    NMDhcpDhclient *       self      = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate *priv      = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
-    GBytes *               duid      = NULL;
-    gs_free char *         leasefile = NULL;
-    GError *               error     = NULL;
+    NMDhcpDhclient           *self = NM_DHCP_DHCLIENT(client);
+    NMDhcpDhclientPrivate    *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    const NMDhcpClientConfig *client_config;
+    GBytes                   *duid      = NULL;
+    gs_free char             *leasefile = NULL;
+    GError                   *error     = NULL;
+
+    client_config = nm_dhcp_client_get_config(client);
 
     /* Look in interface-specific leasefile first for backwards compat */
     leasefile = get_dhclient_leasefile(AF_INET6,
                                        nm_dhcp_client_get_iface(client),
-                                       nm_dhcp_client_get_uuid(client),
+                                       client_config->uuid,
                                        NULL);
     if (leasefile) {
         _LOGD("looking for DUID in '%s'", leasefile);
@@ -719,7 +712,7 @@ static void
 nm_dhcp_dhclient_class_init(NMDhcpDhclientClass *dhclient_class)
 {
     NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhclient_class);
-    GObjectClass *     object_class = G_OBJECT_CLASS(dhclient_class);
+    GObjectClass      *object_class = G_OBJECT_CLASS(dhclient_class);
 
     object_class->dispose = dispose;
 
diff --git a/src/core/dhcp/nm-dhcp-dhcpcanon.c b/src/core/dhcp/nm-dhcp-dhcpcanon.c
index f993ffb9..87e43fa8 100644
--- a/src/core/dhcp/nm-dhcp-dhcpcanon.c
+++ b/src/core/dhcp/nm-dhcp-dhcpcanon.c
@@ -34,10 +34,10 @@ static GType nm_dhcp_dhcpcanon_get_type(void);
 /*****************************************************************************/
 
 typedef struct {
-    char *          conf_file;
-    const char *    def_leasefile;
-    char *          lease_file;
-    char *          pid_file;
+    char           *conf_file;
+    const char     *def_leasefile;
+    char           *lease_file;
+    char           *pid_file;
     NMDhcpListener *dhcp_listener;
 } NMDhcpDhcpcanonPrivate;
 
@@ -65,25 +65,25 @@ nm_dhcp_dhcpcanon_get_path(void)
 
 static gboolean
 dhcpcanon_start(NMDhcpClient *client,
-                const char *  mode_opt,
-                GBytes *      duid,
+                const char   *mode_opt,
+                GBytes       *duid,
                 gboolean      release,
-                pid_t *       out_pid,
+                pid_t        *out_pid,
                 guint         needed_prefixes,
-                GError **     error)
+                GError      **error)
 {
-    NMDhcpDhcpcanon *       self      = NM_DHCP_DHCPCANON(client);
-    NMDhcpDhcpcanonPrivate *priv      = NM_DHCP_DHCPCANON_GET_PRIVATE(self);
+    NMDhcpDhcpcanon             *self = NM_DHCP_DHCPCANON(client);
+    NMDhcpDhcpcanonPrivate      *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(self);
     gs_unref_ptrarray GPtrArray *argv = NULL;
     pid_t                        pid;
-    gs_free_error GError *local = NULL;
-    const char *          iface;
-    const char *          system_bus_address;
-    const char *          dhcpcanon_path;
-    gs_free char *        binary_name            = NULL;
-    gs_free char *        pid_file               = NULL;
-    gs_free char *        system_bus_address_env = NULL;
-    int                   addr_family;
+    gs_free_error GError        *local = NULL;
+    const char                  *iface;
+    const char                  *system_bus_address;
+    const char                  *dhcpcanon_path;
+    gs_free char                *binary_name            = NULL;
+    gs_free char                *pid_file               = NULL;
+    gs_free char                *system_bus_address_env = NULL;
+    int                          addr_family;
 
     g_return_val_if_fail(!priv->pid_file, FALSE);
 
@@ -160,7 +160,7 @@ dhcpcanon_start(NMDhcpClient *client,
 }
 
 static gboolean
-ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
+ip4_start(NMDhcpClient *client, GError **error)
 {
     return dhcpcanon_start(client, NULL, NULL, FALSE, NULL, 0, error);
 }
@@ -168,7 +168,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
 static void
 stop(NMDhcpClient *client, gboolean release)
 {
-    NMDhcpDhcpcanon *       self = NM_DHCP_DHCPCANON(client);
+    NMDhcpDhcpcanon        *self = NM_DHCP_DHCPCANON(client);
     NMDhcpDhcpcanonPrivate *priv = NM_DHCP_DHCPCANON_GET_PRIVATE(self);
     int                     errsv;
 
@@ -222,7 +222,7 @@ static void
 nm_dhcp_dhcpcanon_class_init(NMDhcpDhcpcanonClass *dhcpcanon_class)
 {
     NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhcpcanon_class);
-    GObjectClass *     object_class = G_OBJECT_CLASS(dhcpcanon_class);
+    GObjectClass      *object_class = G_OBJECT_CLASS(dhcpcanon_class);
 
     object_class->dispose = dispose;
 
diff --git a/src/core/dhcp/nm-dhcp-dhcpcd.c b/src/core/dhcp/nm-dhcp-dhcpcd.c
index 7522156b..45f96ec9 100644
--- a/src/core/dhcp/nm-dhcp-dhcpcd.c
+++ b/src/core/dhcp/nm-dhcp-dhcpcd.c
@@ -64,21 +64,19 @@ nm_dhcp_dhcpcd_get_path(void)
 }
 
 static gboolean
-ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
+ip4_start(NMDhcpClient *client, GError **error)
 {
-    NMDhcpDhcpcd *    self            = NM_DHCP_DHCPCD(client);
+    NMDhcpDhcpcd                *self = NM_DHCP_DHCPCD(client);
+    const NMDhcpClientConfig    *client_config;
     gs_unref_ptrarray GPtrArray *argv = NULL;
     pid_t                        pid;
-    GError *                     local;
-    gs_free char *               cmd_str = NULL;
-    const char *                 iface;
-    const char *                 dhcpcd_path;
-    const char *                 hostname;
+    GError                      *local;
+    gs_free char                *cmd_str = NULL;
+    const char                  *dhcpcd_path;
 
     pid = nm_dhcp_client_get_pid(client);
     g_return_val_if_fail(pid == -1, FALSE);
-
-    iface = nm_dhcp_client_get_iface(client);
+    client_config = nm_dhcp_client_get_config(client);
 
     dhcpcd_path = nm_dhcp_dhcpcd_get_path();
     if (!dhcpcd_path) {
@@ -115,21 +113,19 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
      */
     g_ptr_array_add(argv, (gpointer) "-4");
 
-    hostname = nm_dhcp_client_get_hostname(client);
-
-    if (hostname) {
-        if (NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(client), NM_DHCP_CLIENT_FLAGS_USE_FQDN)) {
+    if (client_config->hostname) {
+        if (client_config->use_fqdn) {
             g_ptr_array_add(argv, (gpointer) "-h");
-            g_ptr_array_add(argv, (gpointer) hostname);
+            g_ptr_array_add(argv, (gpointer) client_config->hostname);
             g_ptr_array_add(argv, (gpointer) "-F");
             g_ptr_array_add(argv, (gpointer) "both");
         } else {
             g_ptr_array_add(argv, (gpointer) "-h");
-            g_ptr_array_add(argv, (gpointer) hostname);
+            g_ptr_array_add(argv, (gpointer) client_config->hostname);
         }
     }
 
-    g_ptr_array_add(argv, (gpointer) iface);
+    g_ptr_array_add(argv, (gpointer) client_config->iface);
     g_ptr_array_add(argv, NULL);
 
     _LOGD("running: %s", (cmd_str = g_strjoinv(" ", (char **) argv->pdata)));
@@ -223,7 +219,7 @@ static void
 nm_dhcp_dhcpcd_class_init(NMDhcpDhcpcdClass *dhcpcd_class)
 {
     NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(dhcpcd_class);
-    GObjectClass *     object_class = G_OBJECT_CLASS(dhcpcd_class);
+    GObjectClass      *object_class = G_OBJECT_CLASS(dhcpcd_class);
 
     object_class->dispose = dispose;
 
diff --git a/src/core/dhcp/nm-dhcp-helper.c b/src/core/dhcp/nm-dhcp-helper.c
index fd666676..41862f2b 100644
--- a/src/core/dhcp/nm-dhcp-helper.c
+++ b/src/core/dhcp/nm-dhcp-helper.c
@@ -61,9 +61,9 @@ build_signal_parameters(void)
     for (environ_iter = (const char *const *) environ; *environ_iter; environ_iter++) {
         static const char *const ignore_with_prefix_list[] =
             {"PATH", "SHLVL", "_", "PWD", "dhc_dbus", NULL};
-        const char *       item = *environ_iter;
-        gs_free char *     name = NULL;
-        const char *       val;
+        const char        *item = *environ_iter;
+        gs_free char      *name = NULL;
+        const char        *val;
         const char *const *p;
 
         val = strchr(item, '=');
@@ -119,13 +119,13 @@ 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;
-    guint                      try_count;
-    gint64                     time_start;
-    gint64                     time_end;
+    gs_free_error GError            *error      = NULL;
+    gs_unref_variant GVariant       *parameters = NULL;
+    gs_unref_variant GVariant       *result     = NULL;
+    gboolean                         success    = FALSE;
+    guint                            try_count;
+    gint64                           time_start;
+    gint64                           time_end;
 
     /* 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
diff --git a/src/core/dhcp/nm-dhcp-listener.c b/src/core/dhcp/nm-dhcp-listener.c
index ae2c40f1..2c567593 100644
--- a/src/core/dhcp/nm-dhcp-listener.c
+++ b/src/core/dhcp/nm-dhcp-listener.c
@@ -50,7 +50,7 @@ typedef struct {
     NMDBusManager *dbus_mgr;
     gulong         new_conn_id;
     gulong         dis_conn_id;
-    GHashTable *   connections;
+    GHashTable    *connections;
 } NMDhcpListenerPrivate;
 
 struct _NMDhcpListener {
@@ -98,10 +98,10 @@ NM_DEFINE_SINGLETON_GETTER(NMDhcpListener, nm_dhcp_listener_get, NM_TYPE_DHCP_LI
 static char *
 get_option(GVariant *options, const char *key)
 {
-    GVariant *    value;
+    GVariant     *value;
     const guchar *bytes, *s;
     gsize         len;
-    char *        converted, *d;
+    char         *converted, *d;
 
     if (!g_variant_lookup(options, key, "@ay", &value))
         return NULL;
@@ -130,9 +130,9 @@ get_option(GVariant *options, const char *key)
 static void
 _method_call_handle(NMDhcpListener *self, GVariant *parameters)
 {
-    gs_free char *   iface             = NULL;
-    gs_free char *   pid_str           = NULL;
-    gs_free char *   reason            = NULL;
+    gs_free char              *iface   = NULL;
+    gs_free char              *pid_str = NULL;
+    gs_free char              *reason  = NULL;
     gs_unref_variant GVariant *options = NULL;
     int                        pid;
     gboolean                   handled = FALSE;
@@ -169,12 +169,12 @@ _method_call_handle(NMDhcpListener *self, GVariant *parameters)
 }
 
 static void
-_method_call(GDBusConnection *      connection,
-             const char *           sender,
-             const char *           object_path,
-             const char *           interface_name,
-             const char *           method_name,
-             GVariant *             parameters,
+_method_call(GDBusConnection       *connection,
+             const char            *sender,
+             const char            *object_path,
+             const char            *interface_name,
+             const char            *method_name,
+             GVariant              *parameters,
              GDBusMethodInvocation *invocation,
              gpointer               user_data)
 {
@@ -219,14 +219,14 @@ _dbus_connection_register_object(NMDhcpListener *self, GDBusConnection *connecti
 }
 
 static void
-new_connection_cb(NMDBusManager *     mgr,
-                  GDBusConnection *   connection,
+new_connection_cb(NMDBusManager      *mgr,
+                  GDBusConnection    *connection,
                   GDBusObjectManager *manager,
-                  NMDhcpListener *    self)
+                  NMDhcpListener     *self)
 {
     NMDhcpListenerPrivate *priv = NM_DHCP_LISTENER_GET_PRIVATE(self);
     guint                  registration_id;
-    GError *               error = NULL;
+    GError                *error = NULL;
 
     /* it is important to register the object during the new-connection signal,
      * as this avoids races with the connecting object. */
diff --git a/src/core/dhcp/nm-dhcp-manager.c b/src/core/dhcp/nm-dhcp-manager.c
index 3cb89393..f353e637 100644
--- a/src/core/dhcp/nm-dhcp-manager.c
+++ b/src/core/dhcp/nm-dhcp-manager.c
@@ -17,7 +17,6 @@
 #include <stdio.h>
 
 #include "libnm-glib-aux/nm-dedup-multi.h"
-#include "libnm-systemd-shared/nm-sd-utils-shared.h"
 
 #include "nm-config.h"
 #include "NetworkManagerUtils.h"
@@ -26,7 +25,6 @@
 
 typedef struct {
     const NMDhcpClientFactory *client_factory;
-    char *                     default_hostname;
 } NMDhcpManagerPrivate;
 
 struct _NMDhcpManager {
@@ -129,136 +127,54 @@ _client_factory_get_gtype(const NMDhcpClientFactory *client_factory, int addr_fa
 
 /*****************************************************************************/
 
-static NMDhcpClient *
-client_start(NMDhcpManager *           self,
-             int                       addr_family,
-             NMDedupMultiIndex *       multi_idx,
-             const char *              iface,
-             int                       ifindex,
-             GBytes *                  hwaddr,
-             GBytes *                  bcast_hwaddr,
-             const char *              uuid,
-             guint32                   route_table,
-             guint32                   route_metric,
-             const struct in6_addr *   ipv6_ll_addr,
-             GBytes *                  dhcp_client_id,
-             gboolean                  enforce_duid,
-             guint32                   iaid,
-             gboolean                  iaid_explicit,
-             guint32                   timeout,
-             NMDhcpClientFlags         client_flags,
-             const char *              anycast_address,
-             const char *              hostname,
-             NMDhcpHostnameFlags       hostname_flags,
-             const char *              mud_url,
-             NMSettingIP6ConfigPrivacy privacy,
-             const char *              last_ip4_address,
-             guint                     needed_prefixes,
-             GBytes *                  vendor_class_identifier,
-             const char *const *       reject_servers,
-             GError **                 error)
+NMDhcpClient *
+nm_dhcp_manager_start_client(NMDhcpManager *self, NMDhcpClientConfig *config, GError **error)
 {
-    NMDhcpManagerPrivate *priv;
+    NMDhcpManagerPrivate         *priv;
     gs_unref_object NMDhcpClient *client  = NULL;
     gboolean                      success = FALSE;
     gsize                         hwaddr_len;
     GType                         gtype;
 
     g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL);
-    g_return_val_if_fail(iface, NULL);
-    g_return_val_if_fail(ifindex > 0, NULL);
-    g_return_val_if_fail(uuid != NULL, NULL);
-    g_return_val_if_fail(!dhcp_client_id || g_bytes_get_size(dhcp_client_id) >= 2, NULL);
-    g_return_val_if_fail(!vendor_class_identifier
-                             || g_bytes_get_size(vendor_class_identifier) <= 255,
+    g_return_val_if_fail(config, NULL);
+    g_return_val_if_fail(config->iface, NULL);
+    g_return_val_if_fail(config->l3cfg, NULL);
+    g_return_val_if_fail(config->uuid != NULL, NULL);
+    g_return_val_if_fail(!config->client_id || g_bytes_get_size(config->client_id) >= 2, NULL);
+    g_return_val_if_fail(!config->vendor_class_identifier
+                             || g_bytes_get_size(config->vendor_class_identifier) <= 255,
                          NULL);
     g_return_val_if_fail(!error || !*error, NULL);
-    nm_assert(!NM_FLAGS_ANY(client_flags, ~NM_DHCP_CLIENT_FLAGS_ALL));
 
-    if (addr_family == AF_INET) {
-        if (!hwaddr || !bcast_hwaddr) {
+    if (config->addr_family == AF_INET) {
+        if (!config->hwaddr || !config->bcast_hwaddr) {
             nm_utils_error_set(error,
                                NM_UTILS_ERROR_UNKNOWN,
                                "missing %s address",
-                               hwaddr ? "broadcast" : "MAC");
+                               config->hwaddr ? "broadcast" : "MAC");
             return NULL;
         }
 
-        hwaddr_len = g_bytes_get_size(hwaddr);
+        hwaddr_len = g_bytes_get_size(config->hwaddr);
         if (hwaddr_len == 0 || hwaddr_len > _NM_UTILS_HWADDR_LEN_MAX) {
             nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "invalid MAC address");
             g_return_val_if_reached(NULL);
         }
-        nm_assert(g_bytes_get_size(hwaddr) == g_bytes_get_size(bcast_hwaddr));
-    } else {
-        hwaddr       = NULL;
-        bcast_hwaddr = NULL;
-    }
-
-    if (hostname) {
-        gboolean use_fqdn = NM_FLAGS_HAS(client_flags, NM_DHCP_CLIENT_FLAGS_USE_FQDN);
-
-        if ((use_fqdn && !nm_sd_dns_name_is_valid(hostname))
-            || (!use_fqdn && !nm_sd_hostname_is_valid(hostname, FALSE))) {
-            nm_log_warn(LOGD_DHCP,
-                        "dhcp%c: %s '%s' is invalid, will be ignored",
-                        nm_utils_addr_family_to_char(addr_family),
-                        use_fqdn ? "FQDN" : "hostname",
-                        hostname);
-            hostname = NULL;
-        }
+        nm_assert(g_bytes_get_size(config->hwaddr) == g_bytes_get_size(config->bcast_hwaddr));
     }
 
     priv = NM_DHCP_MANAGER_GET_PRIVATE(self);
 
-    gtype = _client_factory_get_gtype(priv->client_factory, addr_family);
+    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(addr_family),
-                 nm_utils_addr_family_to_char(addr_family),
+                 nm_utils_addr_family_to_char(config->addr_family),
+                 nm_utils_addr_family_to_char(config->addr_family),
                  g_type_name(gtype));
 
-    client = g_object_new(gtype,
-                          NM_DHCP_CLIENT_MULTI_IDX,
-                          multi_idx,
-                          NM_DHCP_CLIENT_ADDR_FAMILY,
-                          addr_family,
-                          NM_DHCP_CLIENT_INTERFACE,
-                          iface,
-                          NM_DHCP_CLIENT_IFINDEX,
-                          ifindex,
-                          NM_DHCP_CLIENT_HWADDR,
-                          hwaddr,
-                          NM_DHCP_CLIENT_BROADCAST_HWADDR,
-                          bcast_hwaddr,
-                          NM_DHCP_CLIENT_UUID,
-                          uuid,
-                          NM_DHCP_CLIENT_IAID,
-                          (guint) iaid,
-                          NM_DHCP_CLIENT_IAID_EXPLICIT,
-                          iaid_explicit,
-                          NM_DHCP_CLIENT_HOSTNAME,
-                          hostname,
-                          NM_DHCP_CLIENT_MUD_URL,
-                          mud_url,
-                          NM_DHCP_CLIENT_ROUTE_TABLE,
-                          (guint) route_table,
-                          NM_DHCP_CLIENT_ROUTE_METRIC,
-                          (guint) route_metric,
-                          NM_DHCP_CLIENT_TIMEOUT,
-                          (guint) timeout,
-                          NM_DHCP_CLIENT_HOSTNAME_FLAGS,
-                          (guint) hostname_flags,
-                          NM_DHCP_CLIENT_VENDOR_CLASS_IDENTIFIER,
-                          vendor_class_identifier,
-                          NM_DHCP_CLIENT_REJECT_SERVERS,
-                          reject_servers,
-                          NM_DHCP_CLIENT_FLAGS,
-                          (guint) client_flags,
-                          NM_DHCP_CLIENT_ANYCAST_ADDRESS,
-                          anycast_address,
-                          NULL);
+    client = g_object_new(gtype, NM_DHCP_CLIENT_CONFIG, config, NULL);
 
     /* unfortunately, our implementations work differently per address-family regarding client-id/DUID.
      *
@@ -286,16 +202,10 @@ client_start(NMDhcpManager *           self,
      * default outside of NetworkManager API.
      */
 
-    if (addr_family == AF_INET) {
-        success = nm_dhcp_client_start_ip4(client, dhcp_client_id, last_ip4_address, error);
+    if (config->addr_family == AF_INET) {
+        success = nm_dhcp_client_start_ip4(client, error);
     } else {
-        success = nm_dhcp_client_start_ip6(client,
-                                           dhcp_client_id,
-                                           enforce_duid,
-                                           ipv6_ll_addr,
-                                           privacy,
-                                           needed_prefixes,
-                                           error);
+        success = nm_dhcp_client_start_ip6(client, error);
     }
 
     if (!success)
@@ -304,178 +214,6 @@ client_start(NMDhcpManager *           self,
     return g_steal_pointer(&client);
 }
 
-/* Caller owns a reference to the NMDhcpClient on return */
-NMDhcpClient *
-nm_dhcp_manager_start_ip4(NMDhcpManager *     self,
-                          NMDedupMultiIndex * multi_idx,
-                          const char *        iface,
-                          int                 ifindex,
-                          GBytes *            hwaddr,
-                          GBytes *            bcast_hwaddr,
-                          const char *        uuid,
-                          guint32             route_table,
-                          guint32             route_metric,
-                          NMDhcpClientFlags   client_flags,
-                          gboolean            send_hostname,
-                          const char *        dhcp_hostname,
-                          const char *        dhcp_fqdn,
-                          NMDhcpHostnameFlags hostname_flags,
-                          const char *        mud_url,
-                          GBytes *            dhcp_client_id,
-                          guint32             timeout,
-                          const char *        anycast_address,
-                          const char *        last_ip_address,
-                          GBytes *            vendor_class_identifier,
-                          const char *const * reject_servers,
-                          GError **           error)
-{
-    NMDhcpManagerPrivate *priv;
-    const char *          hostname     = NULL;
-    gs_free char *        hostname_tmp = NULL;
-    gboolean              use_fqdn     = FALSE;
-    char *                dot;
-
-    /* these flags are set automatically/prohibited, and not free to set to the caller.  */
-    nm_assert(!NM_FLAGS_ANY(client_flags,
-                            NM_DHCP_CLIENT_FLAGS_USE_FQDN | NM_DHCP_CLIENT_FLAGS_INFO_ONLY));
-
-    g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL);
-    priv = NM_DHCP_MANAGER_GET_PRIVATE(self);
-
-    if (send_hostname) {
-        /* Use, in order of preference:
-         *  1. FQDN from configuration
-         *  2. hostname from configuration
-         *  3. system hostname (only host part)
-         */
-        if (dhcp_fqdn) {
-            hostname = dhcp_fqdn;
-            use_fqdn = TRUE;
-        } else if (dhcp_hostname)
-            hostname = dhcp_hostname;
-        else {
-            hostname = priv->default_hostname;
-            if (hostname) {
-                hostname_tmp = g_strdup(hostname);
-                dot          = strchr(hostname_tmp, '.');
-                if (dot)
-                    *dot = '\0';
-                hostname = hostname_tmp;
-            }
-        }
-    }
-
-    return client_start(
-        self,
-        AF_INET,
-        multi_idx,
-        iface,
-        ifindex,
-        hwaddr,
-        bcast_hwaddr,
-        uuid,
-        route_table,
-        route_metric,
-        NULL,
-        dhcp_client_id,
-        FALSE,
-        0,
-        FALSE,
-        timeout,
-        client_flags | (use_fqdn ? NM_DHCP_CLIENT_FLAGS_USE_FQDN : NM_DHCP_CLIENT_FLAGS_NONE),
-        anycast_address,
-        hostname,
-        hostname_flags,
-        mud_url,
-        0,
-        last_ip_address,
-        0,
-        vendor_class_identifier,
-        reject_servers,
-        error);
-}
-
-/* Caller owns a reference to the NMDhcpClient on return */
-NMDhcpClient *
-nm_dhcp_manager_start_ip6(NMDhcpManager *           self,
-                          NMDedupMultiIndex *       multi_idx,
-                          const char *              iface,
-                          int                       ifindex,
-                          const struct in6_addr *   ll_addr,
-                          const char *              uuid,
-                          guint32                   route_table,
-                          guint32                   route_metric,
-                          NMDhcpClientFlags         client_flags,
-                          gboolean                  send_hostname,
-                          const char *              dhcp_hostname,
-                          NMDhcpHostnameFlags       hostname_flags,
-                          const char *              mud_url,
-                          GBytes *                  duid,
-                          gboolean                  enforce_duid,
-                          guint32                   iaid,
-                          gboolean                  iaid_explicit,
-                          guint32                   timeout,
-                          const char *              anycast_address,
-                          NMSettingIP6ConfigPrivacy privacy,
-                          guint                     needed_prefixes,
-                          GError **                 error)
-{
-    NMDhcpManagerPrivate *priv;
-    const char *          hostname = NULL;
-
-    /* this flag is set automatically, and not free to set to the caller.  */
-    nm_assert(!NM_FLAGS_ANY(client_flags, NM_DHCP_CLIENT_FLAGS_USE_FQDN));
-
-    g_return_val_if_fail(NM_IS_DHCP_MANAGER(self), NULL);
-    priv = NM_DHCP_MANAGER_GET_PRIVATE(self);
-
-    if (send_hostname) {
-        /* Always prefer the explicit dhcp-hostname if given */
-        hostname = dhcp_hostname ?: priv->default_hostname;
-    }
-    return client_start(self,
-                        AF_INET6,
-                        multi_idx,
-                        iface,
-                        ifindex,
-                        NULL,
-                        NULL,
-                        uuid,
-                        route_table,
-                        route_metric,
-                        ll_addr,
-                        duid,
-                        enforce_duid,
-                        iaid,
-                        iaid_explicit,
-                        timeout,
-                        client_flags | NM_DHCP_CLIENT_FLAGS_USE_FQDN,
-                        anycast_address,
-                        hostname,
-                        hostname_flags,
-                        mud_url,
-                        privacy,
-                        NULL,
-                        needed_prefixes,
-                        NULL,
-                        NULL,
-                        error);
-}
-
-void
-nm_dhcp_manager_set_default_hostname(NMDhcpManager *manager, const char *hostname)
-{
-    NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE(manager);
-
-    nm_clear_g_free(&priv->default_hostname);
-
-    /* Never send 'localhost'-type names to the DHCP server */
-    if (!nm_utils_is_specific_hostname(hostname))
-        return;
-
-    priv->default_hostname = g_strdup(hostname);
-}
-
 const char *
 nm_dhcp_manager_get_config(NMDhcpManager *self)
 {
@@ -500,10 +238,10 @@ nmtst_dhcp_manager_unget(gpointer self)
 static void
 nm_dhcp_manager_init(NMDhcpManager *self)
 {
-    NMDhcpManagerPrivate *     priv        = NM_DHCP_MANAGER_GET_PRIVATE(self);
-    NMConfig *                 config      = nm_config_get();
-    gs_free char *             client_free = NULL;
-    const char *               client;
+    NMDhcpManagerPrivate      *priv        = NM_DHCP_MANAGER_GET_PRIVATE(self);
+    NMConfig                  *config      = nm_config_get();
+    gs_free char              *client_free = NULL;
+    const char                *client;
     int                        i;
     const NMDhcpClientFactory *client_factory = NULL;
 
@@ -527,37 +265,30 @@ nm_dhcp_manager_init(NMDhcpManager *self)
                                  NM_CONFIG_KEYFILE_KEY_MAIN_DHCP,
                                  NM_CONFIG_GET_VALUE_STRIP | NM_CONFIG_GET_VALUE_NO_EMPTY);
     client = client_free;
-    if (nm_config_get_configure_and_quit(config) == NM_CONFIG_CONFIGURE_AND_QUIT_ENABLED) {
-        client_factory = &_nm_dhcp_client_factory_internal;
-        if (client && !nm_streq(client, client_factory->name))
-            nm_log_info(LOGD_DHCP,
-                        "dhcp-init: Using internal DHCP client since configure-and-quit is set.");
-    } else {
-        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);
-        }
-        if (!client_factory) {
-            client_factory = _client_factory_find_by_name("" NM_CONFIG_DEFAULT_MAIN_DHCP);
+    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);
+    }
+    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);
+        else {
+            client_factory = _client_factory_available(client_factory);
             if (!client_factory)
-                nm_log_err(LOGD_DHCP,
-                           "dhcp-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);
-            }
+                nm_log_info(LOGD_DHCP,
+                            "dhcp-init: default DHCP client '%s' is not available",
+                            NM_CONFIG_DEFAULT_MAIN_DHCP);
         }
-        if (!client_factory) {
-            for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_manager_factories); i++) {
-                client_factory = _client_factory_available(_nm_dhcp_manager_factories[i]);
-                if (client_factory)
-                    break;
-            }
+    }
+    if (!client_factory) {
+        for (i = 0; i < (int) G_N_ELEMENTS(_nm_dhcp_manager_factories); i++) {
+            client_factory = _client_factory_available(_nm_dhcp_manager_factories[i]);
+            if (client_factory)
+                break;
         }
     }
 
@@ -573,20 +304,5 @@ nm_dhcp_manager_init(NMDhcpManager *self)
 }
 
 static void
-dispose(GObject *object)
-{
-    NMDhcpManager *       self = NM_DHCP_MANAGER(object);
-    NMDhcpManagerPrivate *priv = NM_DHCP_MANAGER_GET_PRIVATE(self);
-
-    G_OBJECT_CLASS(nm_dhcp_manager_parent_class)->dispose(object);
-
-    nm_clear_g_free(&priv->default_hostname);
-}
-
-static void
 nm_dhcp_manager_class_init(NMDhcpManagerClass *manager_class)
-{
-    GObjectClass *object_class = G_OBJECT_CLASS(manager_class);
-
-    object_class->dispose = dispose;
-}
+{}
diff --git a/src/core/dhcp/nm-dhcp-manager.h b/src/core/dhcp/nm-dhcp-manager.h
index ce160437..bbd68236 100644
--- a/src/core/dhcp/nm-dhcp-manager.h
+++ b/src/core/dhcp/nm-dhcp-manager.h
@@ -8,7 +8,6 @@
 #define __NETWORKMANAGER_DHCP_MANAGER_H__
 
 #include "nm-dhcp-client.h"
-#include "nm-ip4-config.h"
 #include "nm-dhcp-config.h"
 
 #define NM_TYPE_DHCP_MANAGER (nm_dhcp_manager_get_type())
@@ -32,51 +31,8 @@ const char *nm_dhcp_manager_get_config(NMDhcpManager *self);
 
 void nm_dhcp_manager_set_default_hostname(NMDhcpManager *manager, const char *hostname);
 
-NMDhcpClient *nm_dhcp_manager_start_ip4(NMDhcpManager *            manager,
-                                        struct _NMDedupMultiIndex *multi_idx,
-                                        const char *               iface,
-                                        int                        ifindex,
-                                        GBytes *                   hwaddr,
-                                        GBytes *                   bcast_hwaddr,
-                                        const char *               uuid,
-                                        guint32                    route_table,
-                                        guint32                    route_metric,
-                                        NMDhcpClientFlags          client_flags,
-                                        gboolean                   send_hostname,
-                                        const char *               dhcp_hostname,
-                                        const char *               dhcp_fqdn,
-                                        NMDhcpHostnameFlags        hostname_flags,
-                                        const char *               mud_url,
-                                        GBytes *                   dhcp_client_id,
-                                        guint32                    timeout,
-                                        const char *               anycast_address,
-                                        const char *               last_ip_address,
-                                        GBytes *                   vendor_class_identifier,
-                                        const char *const *        reject_servers,
-                                        GError **                  error);
-
-NMDhcpClient *nm_dhcp_manager_start_ip6(NMDhcpManager *            manager,
-                                        struct _NMDedupMultiIndex *multi_idx,
-                                        const char *               iface,
-                                        int                        ifindex,
-                                        const struct in6_addr *    ll_addr,
-                                        const char *               uuid,
-                                        guint32                    route_table,
-                                        guint32                    route_metric,
-                                        NMDhcpClientFlags          client_flags,
-                                        gboolean                   send_hostname,
-                                        const char *               dhcp_hostname,
-                                        NMDhcpHostnameFlags        hostname_flags,
-                                        const char *               mud_url,
-                                        GBytes *                   duid,
-                                        gboolean                   enforce_duid,
-                                        guint32                    iaid,
-                                        gboolean                   iaid_explicit,
-                                        guint32                    timeout,
-                                        const char *               anycast_address,
-                                        NMSettingIP6ConfigPrivacy  privacy,
-                                        guint                      needed_prefixes,
-                                        GError **                  error);
+NMDhcpClient *
+nm_dhcp_manager_start_client(NMDhcpManager *manager, NMDhcpClientConfig *config, GError **error);
 
 /* For testing only */
 extern const char *nm_dhcp_helper_path;
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index 56b485dd..d7fbe356 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -17,6 +17,7 @@
 #include "libnm-std-aux/unaligned.h"
 #include "libnm-glib-aux/nm-str-buf.h"
 
+#include "nm-l3-config-data.h"
 #include "nm-utils.h"
 #include "nm-config.h"
 #include "nm-dhcp-utils.h"
@@ -47,11 +48,11 @@ typedef struct _NMDhcpNettoolsClass NMDhcpNettoolsClass;
 /*****************************************************************************/
 
 typedef struct {
-    NDhcp4Client *     client;
+    NDhcp4Client      *client;
     NDhcp4ClientProbe *probe;
     NDhcp4ClientLease *lease;
-    GSource *          event_source;
-    char *             lease_file;
+    GSource           *event_source;
+    char              *lease_file;
 } NMDhcpNettoolsPrivate;
 
 struct _NMDhcpNettools {
@@ -85,8 +86,8 @@ set_error_nettools(GError **error, int r, const char *message)
 static inline int
 _client_lease_query(NDhcp4ClientLease *lease,
                     uint8_t            option,
-                    const uint8_t **   datap,
-                    size_t *           n_datap)
+                    const uint8_t    **datap,
+                    size_t            *n_datap)
 {
     return n_dhcp4_client_lease_query(lease, option, (guint8 **) datap, n_datap);
 }
@@ -99,11 +100,11 @@ _client_lease_query(NDhcp4ClientLease *lease,
 
 static gboolean
 lease_option_consume_route(const uint8_t **datap,
-                           size_t *        n_datap,
+                           size_t         *n_datap,
                            gboolean        classless,
-                           in_addr_t *     destp,
-                           uint8_t *       plenp,
-                           in_addr_t *     gatewayp)
+                           in_addr_t      *destp,
+                           uint8_t        *plenp,
+                           in_addr_t      *gatewayp)
 {
     in_addr_t      dest;
     in_addr_t      gateway;
@@ -151,9 +152,9 @@ lease_option_consume_route(const uint8_t **datap,
 
 static gboolean
 lease_parse_address(NDhcp4ClientLease *lease,
-                    NMIP4Config *      ip4_config,
-                    GHashTable *       options,
-                    GError **          error)
+                    NML3ConfigData    *l3cd,
+                    GHashTable        *options,
+                    GError           **error)
 {
     struct in_addr a_address;
     in_addr_t      a_netmask;
@@ -163,7 +164,7 @@ lease_parse_address(NDhcp4ClientLease *lease,
     guint32        a_lifetime;
     guint32        a_timestamp;
     guint64        a_expiry;
-    const guint8 * l_data;
+    const guint8  *l_data;
     gsize          l_data_len;
     int            r;
 
@@ -256,26 +257,26 @@ lease_parse_address(NDhcp4ClientLease *lease,
                                           a_next_server.s_addr);
     }
 
-    nm_ip4_config_add_address(ip4_config,
-                              &((const NMPlatformIP4Address){
-                                  .address      = a_address.s_addr,
-                                  .peer_address = a_address.s_addr,
-                                  .plen         = a_plen,
-                                  .addr_source  = NM_IP_CONFIG_SOURCE_DHCP,
-                                  .timestamp    = a_timestamp,
-                                  .lifetime     = a_lifetime,
-                                  .preferred    = a_lifetime,
-                              }));
+    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    = a_timestamp,
+                                        .lifetime     = a_lifetime,
+                                        .preferred    = a_lifetime,
+                                    }));
 
     return TRUE;
 }
 
 static void
-lease_parse_address_list(NDhcp4ClientLease *      lease,
-                         NMIP4Config *            ip4_config,
+lease_parse_address_list(NDhcp4ClientLease       *lease,
+                         NML3ConfigData          *l3cd,
                          NMDhcpOptionDhcp4Options option,
-                         GHashTable *             options,
-                         NMStrBuf *               sbuf)
+                         GHashTable              *options,
+                         NMStrBuf                *sbuf)
 {
     const guint8 *l_data;
     gsize         l_data_len;
@@ -304,13 +305,13 @@ lease_parse_address_list(NDhcp4ClientLease *      lease,
                  * See https://github.com/systemd/systemd/issues/4524. */
                 continue;
             }
-            nm_ip4_config_add_nameserver(ip4_config, addr);
+            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr);
             break;
         case NM_DHCP_OPTION_DHCP4_NIS_SERVERS:
-            nm_ip4_config_add_nis_server(ip4_config, addr);
+            nm_l3_config_data_add_nis_server(l3cd, addr);
             break;
         case NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER:
-            nm_ip4_config_add_wins(ip4_config, addr);
+            nm_l3_config_data_add_wins(l3cd, addr);
             break;
         case NM_DHCP_OPTION_DHCP4_NTP_SERVER:
             break;
@@ -324,11 +325,9 @@ lease_parse_address_list(NDhcp4ClientLease *      lease,
 
 static void
 lease_parse_routes(NDhcp4ClientLease *lease,
-                   NMIP4Config *      ip4_config,
-                   GHashTable *       options,
-                   guint32            route_table,
-                   guint32            route_metric,
-                   NMStrBuf *         sbuf)
+                   NML3ConfigData    *l3cd,
+                   GHashTable        *options,
+                   NMStrBuf          *sbuf)
 {
     char          dest_str[NM_UTILS_INET_ADDRSTRLEN];
     char          gateway_str[NM_UTILS_INET_ADDRSTRLEN];
@@ -336,9 +335,9 @@ lease_parse_routes(NDhcp4ClientLease *lease,
     in_addr_t     gateway;
     uint8_t       plen;
     guint32       m;
-    gboolean      has_router_from_classless = FALSE;
-    gboolean      has_classless             = FALSE;
-    guint32       default_route_metric      = route_metric;
+    gboolean      has_router_from_classless   = FALSE;
+    gboolean      has_classless               = FALSE;
+    guint32       default_route_metric_offset = 0;
     const guint8 *l_data;
     gsize         l_data_len;
     int           r;
@@ -367,26 +366,22 @@ lease_parse_routes(NDhcp4ClientLease *lease,
             if (plen == 0) {
                 /* if there are multiple default routes, we add them with differing
                  * metrics. */
-                m = default_route_metric;
-                if (default_route_metric < G_MAXUINT32)
-                    default_route_metric++;
-
+                m                         = default_route_metric_offset++;
                 has_router_from_classless = TRUE;
-            } else {
-                m = route_metric;
-            }
-
-            nm_ip4_config_add_route(
-                ip4_config,
-                &((const NMPlatformIP4Route){
-                    .network       = dest,
-                    .plen          = plen,
-                    .gateway       = gateway,
-                    .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                    .metric        = m,
-                    .table_coerced = nm_platform_route_table_coerce(route_table),
-                }),
-                NULL);
+            } else
+                m = 0;
+
+            nm_l3_config_data_add_route_4(l3cd,
+                                          &((const NMPlatformIP4Route){
+                                              .network       = dest,
+                                              .plen          = plen,
+                                              .gateway       = gateway,
+                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
+                                              .table_any     = TRUE,
+                                              .table_coerced = 0,
+                                              .metric_any    = TRUE,
+                                              .metric        = m,
+                                          }));
         }
 
         has_classless = TRUE;
@@ -419,17 +414,17 @@ lease_parse_routes(NDhcp4ClientLease *lease,
                 continue;
             }
 
-            nm_ip4_config_add_route(
-                ip4_config,
-                &((const NMPlatformIP4Route){
-                    .network       = dest,
-                    .plen          = plen,
-                    .gateway       = gateway,
-                    .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                    .metric        = route_metric,
-                    .table_coerced = nm_platform_route_table_coerce(route_table),
-                }),
-                NULL);
+            nm_l3_config_data_add_route_4(l3cd,
+                                          &((const NMPlatformIP4Route){
+                                              .network       = dest,
+                                              .plen          = plen,
+                                              .gateway       = gateway,
+                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
+                                              .table_any     = TRUE,
+                                              .table_coerced = 0,
+                                              .metric_any    = TRUE,
+                                              .metric        = 0,
+                                          }));
         }
 
         nm_dhcp_option_add_option(options,
@@ -463,19 +458,17 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 
             /* if there are multiple default routes, we add them with differing
              * metrics. */
-            m = default_route_metric;
-            if (default_route_metric < G_MAXUINT32)
-                default_route_metric++;
-
-            nm_ip4_config_add_route(
-                ip4_config,
-                &((const NMPlatformIP4Route){
-                    .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                    .gateway       = gateway,
-                    .table_coerced = nm_platform_route_table_coerce(route_table),
-                    .metric        = m,
-                }),
-                NULL);
+            m = default_route_metric_offset++;
+
+            nm_l3_config_data_add_route_4(l3cd,
+                                          &((const NMPlatformIP4Route){
+                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
+                                              .gateway       = gateway,
+                                              .table_any     = TRUE,
+                                              .table_coerced = 0,
+                                              .metric_any    = TRUE,
+                                              .metric        = m,
+                                          }));
         }
 
         nm_dhcp_option_add_option(options,
@@ -486,10 +479,10 @@ lease_parse_routes(NDhcp4ClientLease *lease,
 }
 
 static void
-lease_parse_search_domains(NDhcp4ClientLease *lease, NMIP4Config *ip4_config, GHashTable *options)
+lease_parse_search_domains(NDhcp4ClientLease *lease, NML3ConfigData *l3cd, GHashTable *options)
 {
     gs_strfreev char **domains = NULL;
-    const guint8 *     l_data;
+    const guint8      *l_data;
     gsize              l_data_len;
     guint              i;
     int                r;
@@ -504,7 +497,7 @@ lease_parse_search_domains(NDhcp4ClientLease *lease, NMIP4Config *ip4_config, GH
         return;
 
     for (i = 0; domains[i]; i++)
-        nm_ip4_config_add_search(ip4_config, domains[i]);
+        nm_l3_config_data_add_search(l3cd, AF_INET, domains[i]);
 
     nm_dhcp_option_take_option(options,
                                AF_INET,
@@ -539,34 +532,31 @@ lease_parse_private_options(NDhcp4ClientLease *lease, GHashTable *options)
     }
 }
 
-static NMIP4Config *
+static NML3ConfigData *
 lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char *       iface,
+                    const char        *iface,
                     int                ifindex,
                     NDhcp4ClientLease *lease,
-                    guint32            route_table,
-                    guint32            route_metric,
-                    GHashTable **      out_options,
-                    GError **          error)
+                    GError           **error)
 {
-    nm_auto_str_buf NMStrBuf sbuf           = NM_STR_BUF_INIT(0, FALSE);
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    gs_unref_hashtable GHashTable *options  = NULL;
-    const guint8 *                 l_data;
-    gsize                          l_data_len;
-    const char *                   v_str;
-    guint16                        v_u16;
-    gboolean                       v_bool;
-    in_addr_t                      v_inaddr;
-    struct in_addr                 v_inaddr_s;
-    int                            r;
+    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;
+    const guint8                           *l_data;
+    gsize                                   l_data_len;
+    const char                             *v_str;
+    guint16                                 v_u16;
+    in_addr_t                               v_inaddr;
+    struct in_addr                          v_inaddr_s;
+    int                                     r;
 
     g_return_val_if_fail(lease != NULL, NULL);
 
-    ip4_config = nm_ip4_config_new(multi_idx, ifindex);
-    options    = nm_dhcp_option_create_options_dict();
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+
+    options = nm_dhcp_option_create_options_dict();
 
-    if (!lease_parse_address(lease, ip4_config, options, error))
+    if (!lease_parse_address(lease, l3cd, options, error))
         return NULL;
 
     r = n_dhcp4_client_lease_get_server_identifier(lease, &v_inaddr_s);
@@ -585,13 +575,9 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                                           v_inaddr);
     }
 
-    lease_parse_routes(lease, ip4_config, options, route_table, route_metric, &sbuf);
+    lease_parse_routes(lease, l3cd, options, &sbuf);
 
-    lease_parse_address_list(lease,
-                             ip4_config,
-                             NM_DHCP_OPTION_DHCP4_DOMAIN_NAME_SERVER,
-                             options,
-                             &sbuf);
+    lease_parse_address_list(lease, l3cd, 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)) {
@@ -616,7 +602,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
                 nm_str_buf_append_required_delimiter(&sbuf, ' ');
                 nm_str_buf_append(&sbuf, s);
-                nm_ip4_config_add_domain(ip4_config, s);
+                nm_l3_config_data_add_domain(l3cd, AF_INET, s);
             }
         }
 
@@ -628,18 +614,17 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         }
     }
 
-    lease_parse_search_domains(lease, ip4_config, options);
+    lease_parse_search_domains(lease, l3cd, 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)) {
         nm_dhcp_option_add_option_u64(options, AF_INET, NM_DHCP_OPTION_DHCP4_INTERFACE_MTU, v_u16);
-        nm_ip4_config_set_mtu(ip4_config, v_u16, NM_IP_CONFIG_SOURCE_DHCP);
+        nm_l3_config_data_set_mtu(l3cd, v_u16);
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_VENDOR_SPECIFIC, &l_data, &l_data_len);
-    v_bool =
-        (r == 0) && memmem(l_data, l_data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED"));
-    nm_ip4_config_set_metered(ip4_config, v_bool);
+    if ((r == 0) && memmem(l_data, l_data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED")))
+        nm_l3_config_data_set_metered(l3cd, TRUE);
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_HOST_NAME, &l_data, &l_data_len);
     if (r == 0) {
@@ -650,7 +635,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         }
     }
 
-    lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NTP_SERVER, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, 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)) {
@@ -681,11 +666,17 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
          * We reject NUL characters inside the string (except trailing NULs).
          * Otherwise, we allow any encoding and backslash-escape the result to
          * UTF-8. */
-        nm_dhcp_option_add_option_utf8safe_escape(options,
-                                                  AF_INET,
-                                                  NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
-                                                  l_data,
-                                                  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 ?: "");
     }
 
     r = _client_lease_query(lease, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, &l_data, &l_data_len);
@@ -700,7 +691,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                                              &to_free);
 
         nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NIS_DOMAIN, v_str ?: "");
-        nm_ip4_config_set_nis_domain(ip4_config, v_str ?: "");
+        nm_l3_config_data_set_nis_domain(l3cd, v_str ?: "");
     }
 
     r = n_dhcp4_client_lease_get_file(lease, &v_str);
@@ -728,18 +719,17 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                                   v_str ?: "");
     }
 
-    lease_parse_address_list(lease, ip4_config, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
+    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NIS_SERVERS, options, &sbuf);
 
-    lease_parse_address_list(lease,
-                             ip4_config,
-                             NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER,
-                             options,
-                             &sbuf);
+    lease_parse_address_list(lease, l3cd, NM_DHCP_OPTION_DHCP4_NETBIOS_NAMESERVER, options, &sbuf);
 
     lease_parse_private_options(lease, options);
 
-    NM_SET_OUT(out_options, g_steal_pointer(&options));
-    return g_steal_pointer(&ip4_config);
+    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);
 }
 
 /*****************************************************************************/
@@ -750,7 +740,7 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
     struct in_addr           a_address;
     nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE);
     char                     addr_str[NM_UTILS_INET_ADDRSTRLEN];
-    gs_free_error GError *error = NULL;
+    gs_free_error GError    *error = NULL;
 
     nm_assert(lease);
     nm_assert(lease_file);
@@ -771,47 +761,44 @@ lease_save(NMDhcpNettools *self, NDhcp4ClientLease *lease, const char *lease_fil
 static void
 bound4_handle(NMDhcpNettools *self, NDhcp4ClientLease *lease, gboolean extended)
 {
-    NMDhcpNettoolsPrivate *priv             = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    const char *           iface            = nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self));
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    gs_unref_hashtable GHashTable *options  = NULL;
-    GError *                       error    = NULL;
+    NMDhcpNettoolsPrivate                  *priv   = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    NMDhcpClient                           *client = NM_DHCP_CLIENT(self);
+    const NMDhcpClientConfig               *client_config;
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd  = NULL;
+    GError                                 *error = NULL;
 
     _LOGT("lease available (%s)", extended ? "extended" : "new");
-
-    ip4_config = 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,
-                                     nm_dhcp_client_get_route_table(NM_DHCP_CLIENT(self)),
-                                     nm_dhcp_client_get_route_metric(NM_DHCP_CLIENT(self)),
-                                     &options,
-                                     &error);
-    if (!ip4_config) {
-        _LOGW("%s", error->message);
+    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);
+    if (!l3cd) {
+        _LOGW("failure to parse lease: %s", error->message);
         g_clear_error(&error);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return;
     }
 
-    nm_dhcp_option_add_requests_to_options(options, AF_INET);
     lease_save(self, lease, priv->lease_file);
 
     nm_dhcp_client_set_state(NM_DHCP_CLIENT(self),
                              extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND,
-                             NM_IP_CONFIG_CAST(ip4_config),
-                             options);
+                             l3cd);
 }
 
 static void
 dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
 {
-    NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    struct in_addr         server_id;
-    char                   addr_str[INET_ADDRSTRLEN];
-    int                    r;
+    NMDhcpNettoolsPrivate    *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    const NMDhcpClientConfig *client_config;
+    struct in_addr            server_id;
+    char                      addr_str[INET_ADDRSTRLEN];
+    int                       r;
 
     _LOGT("client event %d", event->event);
+    client_config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));
 
     switch (event->event) {
     case N_DHCP4_CLIENT_EVENT_OFFER:
@@ -835,10 +822,10 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
         break;
     case N_DHCP4_CLIENT_EVENT_RETRACTED:
     case N_DHCP4_CLIENT_EVENT_EXPIRED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_EXPIRE, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_EXPIRE, NULL);
         break;
     case N_DHCP4_CLIENT_EVENT_CANCELLED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         break;
     case N_DHCP4_CLIENT_EVENT_GRANTED:
         priv->lease = n_dhcp4_client_lease_ref(event->granted.lease);
@@ -861,7 +848,7 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
                    NULL,
                    NULL,
                    "dhcp4 (%s): %s",
-                   nm_dhcp_client_get_iface(NM_DHCP_CLIENT(self)),
+                   client_config->iface,
                    event->log.message);
         }
     } break;
@@ -874,9 +861,9 @@ dhcp4_event_handle(NMDhcpNettools *self, NDhcp4ClientEvent *event)
 static gboolean
 dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
 {
-    NMDhcpNettools *       self = user_data;
+    NMDhcpNettools        *self = user_data;
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    NDhcp4ClientEvent *    event;
+    NDhcp4ClientEvent     *event;
     int                    r;
 
     r = n_dhcp4_client_dispatch(priv->client);
@@ -890,13 +877,12 @@ dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
          */
         _LOGE("error %d dispatching events", r);
         nm_clear_g_source_inst(&priv->event_source);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return G_SOURCE_REMOVE;
     }
 
-    while (!n_dhcp4_client_pop_event(priv->client, &event) && event) {
+    while (!n_dhcp4_client_pop_event(priv->client, &event) && event)
         dhcp4_event_handle(self, event);
-    }
 
     return G_SOURCE_CONTINUE;
 }
@@ -906,31 +892,34 @@ nettools_create(NMDhcpNettools *self, GError **error)
 {
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     nm_auto(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
-    nm_auto(n_dhcp4_client_unrefp) NDhcp4Client *            client = NULL;
-    GBytes *                                                 hwaddr;
-    GBytes *                                                 bcast_hwaddr;
-    const uint8_t *                                          hwaddr_arr;
-    const uint8_t *                                          bcast_hwaddr_arr;
+    nm_auto(n_dhcp4_client_unrefp) NDhcp4Client             *client = NULL;
+    GBytes                                                  *hwaddr;
+    GBytes                                                  *bcast_hwaddr;
+    const uint8_t                                           *hwaddr_arr;
+    const uint8_t                                           *bcast_hwaddr_arr;
     gsize                                                    hwaddr_len;
     gsize                                                    bcast_hwaddr_len;
-    GBytes *                                                 client_id;
-    gs_unref_bytes GBytes *client_id_new = NULL;
-    const uint8_t *        client_id_arr;
-    size_t                 client_id_len;
-    int                    r, fd, arp_type, transport;
+    GBytes                                                  *client_id;
+    gs_unref_bytes GBytes                                   *client_id_new = NULL;
+    const uint8_t                                           *client_id_arr;
+    size_t                                                   client_id_len;
+    int                                                      r, fd, arp_type, transport;
+    const NMDhcpClientConfig                                *client_config;
+
+    client_config = nm_dhcp_client_get_config(NM_DHCP_CLIENT(self));
 
     g_return_val_if_fail(!priv->client, FALSE);
 
     /* TODO: honor nm_dhcp_client_get_anycast_address() */
 
-    hwaddr = nm_dhcp_client_get_hw_addr(NM_DHCP_CLIENT(self));
+    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     = nm_dhcp_client_get_broadcast_hw_addr(NM_DHCP_CLIENT(self));
+    bcast_hwaddr     = client_config->bcast_hwaddr;
     bcast_hwaddr_arr = g_bytes_get_data(bcast_hwaddr, &bcast_hwaddr_len);
 
     switch (arp_type) {
@@ -947,7 +936,7 @@ nettools_create(NMDhcpNettools *self, GError **error)
 
     /* Note that we always set a client-id. In particular for infiniband that is necessary,
      * see https://tools.ietf.org/html/rfc4390#section-2.1 . */
-    client_id = nm_dhcp_client_get_client_id(NM_DHCP_CLIENT(self));
+    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;
@@ -971,10 +960,7 @@ nettools_create(NMDhcpNettools *self, GError **error)
     n_dhcp4_client_config_set_transport(config, transport);
     n_dhcp4_client_config_set_mac(config, hwaddr_arr, hwaddr_len);
     n_dhcp4_client_config_set_broadcast_mac(config, bcast_hwaddr_arr, bcast_hwaddr_len);
-    n_dhcp4_client_config_set_request_broadcast(
-        config,
-        NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(NM_DHCP_CLIENT(self)),
-                     NM_DHCP_CLIENT_FLAGS_REQUEST_BROADCAST));
+    n_dhcp4_client_config_set_request_broadcast(config, client_config->v4.request_broadcast);
     r = n_dhcp4_client_config_set_client_id(config,
                                             client_id_arr,
                                             NM_MIN(client_id_len, 1 + _NM_MAX_CLIENT_ID_LEN));
@@ -1005,7 +991,7 @@ nettools_create(NMDhcpNettools *self, GError **error)
 static gboolean
 _accept(NMDhcpClient *client, GError **error)
 {
-    NMDhcpNettools *       self = NM_DHCP_NETTOOLS(client);
+    NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     int                    r;
 
@@ -1027,13 +1013,13 @@ _accept(NMDhcpClient *client, GError **error)
 static gboolean
 decline(NMDhcpClient *client, const char *error_message, GError **error)
 {
-    NMDhcpNettools *       self = NM_DHCP_NETTOOLS(client);
+    NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     int                    r;
 
     g_return_val_if_fail(priv->lease, FALSE);
 
-    _LOGT("dhcp4-client: decline");
+    _LOGT("dhcp4-client: decline (%s)", error_message);
 
     r = n_dhcp4_client_lease_decline(priv->lease, error_message);
     if (r) {
@@ -1063,19 +1049,20 @@ fqdn_flags_to_wire(NMDhcpHostnameFlags flags)
 }
 
 static gboolean
-ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
+ip4_start(NMDhcpClient *client, GError **error)
 {
     nm_auto(n_dhcp4_client_probe_config_freep) NDhcp4ClientProbeConfig *config = NULL;
-    NMDhcpNettools *       self       = NM_DHCP_NETTOOLS(client);
-    NMDhcpNettoolsPrivate *priv       = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
-    gs_free char *         lease_file = NULL;
-    struct in_addr         last_addr  = {0};
-    const char *           hostname;
-    const char *           mud_url;
-    GBytes *               vendor_class_identifier;
-    int                    r, i;
+    NMDhcpNettools           *self = NM_DHCP_NETTOOLS(client);
+    NMDhcpNettoolsPrivate    *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    const NMDhcpClientConfig *client_config;
+    gs_free char             *lease_file = NULL;
+    struct in_addr            last_addr  = {0};
+    int                       r, i;
+
+    client_config = nm_dhcp_client_get_config(client);
 
     g_return_val_if_fail(!priv->probe, FALSE);
+    g_return_val_if_fail(client_config, FALSE);
 
     if (!nettools_create(self, error))
         return FALSE;
@@ -1094,12 +1081,12 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
 
     nm_dhcp_utils_get_leasefile_path(AF_INET,
                                      "internal",
-                                     nm_dhcp_client_get_iface(client),
-                                     nm_dhcp_client_get_uuid(client),
+                                     client_config->iface,
+                                     client_config->uuid,
                                      &lease_file);
 
-    if (last_ip4_address)
-        inet_pton(AF_INET, last_ip4_address, &last_addr);
+    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
@@ -1129,31 +1116,33 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    mud_url = nm_dhcp_client_get_mud_url(client);
-    if (mud_url) {
+    if (client_config->mud_url) {
         r = n_dhcp4_client_probe_config_append_option(config,
                                                       NM_DHCP_OPTION_DHCP4_MUD_URL,
-                                                      mud_url,
-                                                      strlen(mud_url));
+                                                      client_config->mud_url,
+                                                      strlen(client_config->mud_url));
         if (r) {
             set_error_nettools(error, r, "failed to set MUD URL");
             return FALSE;
         }
     }
-    hostname = nm_dhcp_client_get_hostname(client);
-    if (hostname) {
-        if (NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(client), NM_DHCP_CLIENT_FLAGS_USE_FQDN)) {
+
+    if (client_config->hostname) {
+        if (client_config->use_fqdn) {
             uint8_t             buffer[255];
             NMDhcpHostnameFlags flags;
             size_t              fqdn_len;
 
-            flags     = nm_dhcp_client_get_hostname_flags(client);
+            flags     = client_config->hostname_flags;
             buffer[0] = fqdn_flags_to_wire(flags);
             buffer[1] = 0; /* RCODE1 (deprecated) */
             buffer[2] = 0; /* RCODE2 (deprecated) */
 
             if (flags & NM_DHCP_HOSTNAME_FLAG_FQDN_ENCODED) {
-                r = nm_sd_dns_name_to_wire_format(hostname, buffer + 3, sizeof(buffer) - 3, FALSE);
+                r = nm_sd_dns_name_to_wire_format(client_config->hostname,
+                                                  buffer + 3,
+                                                  sizeof(buffer) - 3,
+                                                  FALSE);
                 if (r <= 0) {
                     if (r < 0)
                         nm_utils_error_set_errno(error, r, "failed to convert DHCP FQDN: %s");
@@ -1163,12 +1152,12 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
                 }
                 fqdn_len = r;
             } else {
-                fqdn_len = strlen(hostname);
+                fqdn_len = strlen(client_config->hostname);
                 if (fqdn_len > sizeof(buffer) - 3) {
                     nm_utils_error_set(error, r, "failed to set DHCP FQDN: name too long");
                     return FALSE;
                 }
-                memcpy(buffer + 3, hostname, fqdn_len);
+                memcpy(buffer + 3, client_config->hostname, fqdn_len);
             }
 
             r = n_dhcp4_client_probe_config_append_option(config,
@@ -1182,8 +1171,8 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         } else {
             r = n_dhcp4_client_probe_config_append_option(config,
                                                           NM_DHCP_OPTION_DHCP4_HOST_NAME,
-                                                          hostname,
-                                                          strlen(hostname));
+                                                          client_config->hostname,
+                                                          strlen(client_config->hostname));
             if (r) {
                 set_error_nettools(error, r, "failed to set DHCP hostname");
                 return FALSE;
@@ -1191,12 +1180,11 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    vendor_class_identifier = nm_dhcp_client_get_vendor_class_identifier(client);
-    if (vendor_class_identifier) {
+    if (client_config->vendor_class_identifier) {
         const void *option_data;
         gsize       option_size;
 
-        option_data = g_bytes_get_data(vendor_class_identifier, &option_size);
+        option_data = g_bytes_get_data(client_config->vendor_class_identifier, &option_size);
         nm_assert(option_data);
         nm_assert(option_size <= 255);
 
@@ -1220,15 +1208,13 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
     }
 
     _LOGT("dhcp-client4: start " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(priv->client));
-
-    nm_dhcp_client_start_timeout(client);
     return TRUE;
 }
 
 static void
 stop(NMDhcpClient *client, gboolean release)
 {
-    NMDhcpNettools *       self = NM_DHCP_NETTOOLS(client);
+    NMDhcpNettools        *self = NM_DHCP_NETTOOLS(client);
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
 
     NM_DHCP_CLIENT_CLASS(nm_dhcp_nettools_parent_class)->stop(client, release);
@@ -1262,7 +1248,7 @@ static void
 nm_dhcp_nettools_class_init(NMDhcpNettoolsClass *class)
 {
     NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(class);
-    GObjectClass *     object_class = G_OBJECT_CLASS(class);
+    GObjectClass      *object_class = G_OBJECT_CLASS(class);
 
     object_class->dispose = dispose;
 
diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c
index 011f23e4..96fef81a 100644
--- a/src/core/dhcp/nm-dhcp-options.c
+++ b/src/core/dhcp/nm-dhcp-options.c
@@ -201,6 +201,7 @@ const NMDhcpOption _nm_dhcp_option_dhcp6_options[] = {
     REQ(NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, "dhcp6_domain_search", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_SNTP_SERVERS, "dhcp6_sntp_servers", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_FQDN, "fqdn_fqdn", FALSE),
+    REQ(NM_DHCP_OPTION_DHCP6_NTP_SERVER, "dhcp6_ntp_servers", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_MUD_URL, "dhcp6_mud_url", FALSE),
 
     /* Internal values */
@@ -235,6 +236,7 @@ static const NMDhcpOption *const _sorted_options_6[G_N_ELEMENTS(_nm_dhcp_option_
     A(13),
     A(14),
     A(15),
+    A(16),
 #undef A
 };
 
@@ -247,7 +249,7 @@ _sorted_options_generate_sort(gconstpointer pa, gconstpointer pb, gpointer user_
     const NMDhcpOption *const *b = pb;
 
     NM_CMP_DIRECT((*a)->option_num, (*b)->option_num);
-    return nm_assert_unreachable_val(0);
+    return 0;
 }
 
 static char *
@@ -272,7 +274,6 @@ _sorted_options_generate(const NMDhcpOption *base, const NMDhcpOption *const *so
 
 _nm_unused static void
 _ASSERT_sorted(int IS_IPv4, const NMDhcpOption *const *const sorted, int n)
-
 {
     const NMDhcpOption *const options =
         IS_IPv4 ? _nm_dhcp_option_dhcp4_options : _nm_dhcp_option_dhcp6_options;
@@ -283,6 +284,16 @@ _ASSERT_sorted(int IS_IPv4, const NMDhcpOption *const *const sorted, int n)
     for (i = 0; i < n; i++) {
         const NMDhcpOption *opt = sorted[i];
 
+        if (!opt || opt < options || opt >= &options[n]) {
+            g_error("%s:%d: _sorted_options_%c[%d] must be a valid pointer into "
+                    "_nm_dhcp_option_dhcp%c_options, but is %p",
+                    __FILE__,
+                    __LINE__,
+                    IS_IPv4 ? '4' : '6',
+                    i,
+                    IS_IPv4 ? '4' : '6',
+                    opt);
+        }
         g_assert(opt);
         g_assert(opt >= options);
         g_assert(opt < &options[n]);
@@ -291,7 +302,7 @@ _ASSERT_sorted(int IS_IPv4, const NMDhcpOption *const *const sorted, int n)
             const NMDhcpOption *opt2 = sorted[j];
 
             if (opt == opt2) {
-                g_error("%s:%d: the _sorted_options_%c at [%d] (opt=%u, %s) is duplicated at "
+                g_error("%s:%d: _sorted_options_%c[%d] (opt=%u, %s) is duplicated at "
                         "[%d] (SORT: %s)",
                         __FILE__,
                         __LINE__,
@@ -308,7 +319,7 @@ _ASSERT_sorted(int IS_IPv4, const NMDhcpOption *const *const sorted, int n)
             const NMDhcpOption *opt2 = sorted[i - 1];
 
             if (opt2->option_num >= opt->option_num) {
-                g_error("%s:%d: the _sorted_options_%c at [%d] (opt=%u, %s) should come before "
+                g_error("%s:%d: _sorted_options_%c[%d] (opt=%u, %s) should come before "
                         "[%d] (opt=%u, %s) (SORT: %s)",
                         __FILE__,
                         __LINE__,
@@ -396,14 +407,14 @@ nm_dhcp_option_add_option(GHashTable *options, int addr_family, guint option, co
 }
 
 void
-nm_dhcp_option_add_option_utf8safe_escape(GHashTable *  options,
+nm_dhcp_option_add_option_utf8safe_escape(GHashTable   *options,
                                           int           addr_family,
                                           guint         option,
                                           const guint8 *data,
                                           gsize         n_data)
 {
     gs_free char *to_free = NULL;
-    const char *  escaped;
+    const char   *escaped;
 
     escaped = nm_utils_buf_utf8safe_escape((char *) data,
                                            n_data,
diff --git a/src/core/dhcp/nm-dhcp-options.h b/src/core/dhcp/nm-dhcp-options.h
index 8ed2039d..4c978c4f 100644
--- a/src/core/dhcp/nm-dhcp-options.h
+++ b/src/core/dhcp/nm-dhcp-options.h
@@ -163,6 +163,7 @@ typedef enum {
     NM_DHCP_OPTION_DHCP6_DOMAIN_LIST  = 24,
     NM_DHCP_OPTION_DHCP6_SNTP_SERVERS = 31,
     NM_DHCP_OPTION_DHCP6_FQDN         = 39,
+    NM_DHCP_OPTION_DHCP6_NTP_SERVER   = 56, /* RFC 5908 */
     NM_DHCP_OPTION_DHCP6_MUD_URL      = 112,
 
     /* Internal values */
@@ -187,7 +188,7 @@ typedef struct {
 } NMDhcpOption;
 
 extern const NMDhcpOption _nm_dhcp_option_dhcp4_options[143];
-extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[16];
+extern const NMDhcpOption _nm_dhcp_option_dhcp6_options[17];
 
 static inline const char *
 nm_dhcp_option_get_name(const NMDhcpOption *option)
@@ -210,7 +211,7 @@ nm_dhcp_option_request_string(int addr_family, guint option)
 void nm_dhcp_option_take_option(GHashTable *options, int addr_family, guint option, char *value);
 void
 nm_dhcp_option_add_option(GHashTable *options, int addr_family, guint option, const char *value);
-void nm_dhcp_option_add_option_utf8safe_escape(GHashTable *  options,
+void nm_dhcp_option_add_option_utf8safe_escape(GHashTable   *options,
                                                int           addr_family,
                                                guint         option,
                                                const guint8 *data,
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index af1d2238..0884def3 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -17,6 +17,7 @@
 #include "libnm-std-aux/unaligned.h"
 
 #include "nm-utils.h"
+#include "nm-l3-config-data.h"
 #include "nm-dhcp-utils.h"
 #include "nm-dhcp-options.h"
 #include "nm-core-utils.h"
@@ -46,13 +47,11 @@ static GType nm_dhcp_systemd_get_type(void);
 /*****************************************************************************/
 
 typedef struct {
-    sd_dhcp_client * client4;
+    sd_dhcp_client  *client4;
     sd_dhcp6_client *client6;
-    char *           lease_file;
+    char            *lease_file;
 
     guint request_count;
-
-    bool privacy : 1;
 } NMDhcpSystemdPrivate;
 
 struct _NMDhcpSystemd {
@@ -70,45 +69,41 @@ G_DEFINE_TYPE(NMDhcpSystemd, nm_dhcp_systemd, NM_TYPE_DHCP_CLIENT)
 
 /*****************************************************************************/
 
-static NMIP4Config *
+static NML3ConfigData *
 lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
-                    const char *       iface,
+                    const char        *iface,
                     int                ifindex,
-                    sd_dhcp_lease *    lease,
-                    guint32            route_table,
-                    guint32            route_metric,
-                    GHashTable **      out_options,
-                    GError **          error)
+                    sd_dhcp_lease     *lease,
+                    GError           **error)
 {
-    gs_unref_object NMIP4Config *ip4_config = 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         = NULL;
-    const char *const *          search_domains = NULL;
-    guint16                      mtu;
-    int                          i, num;
-    const void *                 data;
-    gsize                        data_len;
-    gboolean                     metered                   = FALSE;
-    gboolean                     has_router_from_classless = FALSE;
-    gboolean                     has_classless_route       = FALSE;
-    gboolean                     has_static_route          = 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_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         = NULL;
+    const char *const                      *search_domains = NULL;
+    guint16                                 mtu;
+    int                                     i, num;
+    const void                             *data;
+    gsize                                   data_len;
+    gboolean                                has_router_from_classless = FALSE;
+    gboolean                                has_classless_route       = FALSE;
+    gboolean                                has_static_route          = 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);
 
@@ -133,9 +128,9 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         return NULL;
     }
 
-    ip4_config = nm_ip4_config_new(multi_idx, ifindex);
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
 
-    options = out_options ? nm_dhcp_option_create_options_dict() : NULL;
+    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);
@@ -160,16 +155,16 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_NM_NEXT_SERVER, addr_str);
     }
 
-    nm_ip4_config_add_address(ip4_config,
-                              &((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,
-                              }));
+    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);
@@ -193,7 +188,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
                  * See https://github.com/systemd/systemd/issues/4524. */
                 continue;
             }
-            nm_ip4_config_add_nameserver(ip4_config, addr_list[i].s_addr);
+            nm_l3_config_data_add_nameserver(l3cd, AF_INET, &addr_list[i].s_addr);
         }
         nm_dhcp_option_add_option(options,
                                   AF_INET,
@@ -206,7 +201,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         nm_gstring_prepare(&str);
         for (i = 0; i < num; i++) {
             g_string_append(nm_gstring_add_space_delimiter(str), search_domains[i]);
-            nm_ip4_config_add_search(ip4_config, search_domains[i]);
+            nm_l3_config_data_add_search(l3cd, AF_INET, search_domains[i]);
         }
         nm_dhcp_option_add_option(options,
                                   AF_INET,
@@ -216,7 +211,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
     if (sd_dhcp_lease_get_domainname(lease, &s) >= 0) {
         gs_strfreev char **domains = NULL;
-        char **            d;
+        char             **d;
 
         nm_dhcp_option_add_option(options, AF_INET, NM_DHCP_OPTION_DHCP4_DOMAIN_NAME, s);
 
@@ -224,7 +219,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
          * As systemd escapes such characters, split them at \\032. */
         domains = g_strsplit(s, "\\032", 0);
         for (d = domains; *d; d++)
-            nm_ip4_config_add_domain(ip4_config, *d);
+            nm_l3_config_data_add_domain(l3cd, AF_INET, *d);
     }
 
     if (sd_dhcp_lease_get_hostname(lease, &s) >= 0) {
@@ -233,9 +228,9 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
     num = sd_dhcp_lease_get_routes(lease, &routes);
     if (num > 0) {
-        nm_auto_free_gstring GString *str_classless        = NULL;
-        nm_auto_free_gstring GString *str_static           = NULL;
-        guint32                       default_route_metric = route_metric;
+        nm_auto_free_gstring GString *str_classless               = NULL;
+        nm_auto_free_gstring GString *str_static                  = NULL;
+        guint32                       default_route_metric_offset = 0;
 
         for (i = 0; i < num; i++) {
             switch (sd_dhcp_route_get_option(routes[i])) {
@@ -307,25 +302,22 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
             if (r_plen == 0) {
                 /* if there are multiple default routes, we add them with differing
                  * metrics. */
-                m = default_route_metric;
-                if (default_route_metric < G_MAXUINT32)
-                    default_route_metric++;
-
+                m                         = default_route_metric_offset++;
                 has_router_from_classless = TRUE;
             } else
-                m = route_metric;
-
-            nm_ip4_config_add_route(
-                ip4_config,
-                &((const NMPlatformIP4Route){
-                    .network       = network_net,
-                    .plen          = r_plen,
-                    .gateway       = r_gateway.s_addr,
-                    .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                    .metric        = m,
-                    .table_coerced = nm_platform_route_table_coerce(route_table),
-                }),
-                NULL);
+                m = 0;
+
+            nm_l3_config_data_add_route_4(l3cd,
+                                          &((const NMPlatformIP4Route){
+                                              .network       = network_net,
+                                              .plen          = r_plen,
+                                              .gateway       = r_gateway.s_addr,
+                                              .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
+                                              .metric_any    = TRUE,
+                                              .metric        = m,
+                                              .table_any     = TRUE,
+                                              .table_coerced = 0,
+                                          }));
         }
 
         if (str_classless && str_classless->len > 0)
@@ -342,7 +334,7 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
     num = sd_dhcp_lease_get_router(lease, &a_router);
     if (num > 0) {
-        guint32 default_route_metric = route_metric;
+        guint32 default_route_metric_offset = 0;
 
         nm_gstring_prepare(&str);
         for (i = 0; i < num; i++) {
@@ -368,26 +360,24 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 
             /* if there are multiple default routes, we add them with differing
              * metrics. */
-            m = default_route_metric;
-            if (default_route_metric < G_MAXUINT32)
-                default_route_metric++;
-
-            nm_ip4_config_add_route(
-                ip4_config,
-                &((const NMPlatformIP4Route){
-                    .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
-                    .gateway       = a_router[i].s_addr,
-                    .table_coerced = nm_platform_route_table_coerce(route_table),
-                    .metric        = m,
-                }),
-                NULL);
+            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,
+                                              .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_ip4_config_set_mtu(ip4_config, mtu, NM_IP_CONFIG_SOURCE_DHCP);
+        nm_l3_config_data_set_mtu(l3cd, mtu);
     }
 
     num = sd_dhcp_lease_get_ntp(lease, &addr_list);
@@ -422,9 +412,10 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
         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)
-        metered = !!memmem(data, data_len, "ANDROID_METERED", NM_STRLEN("ANDROID_METERED"));
-    nm_ip4_config_set_metered(ip4_config, metered);
+    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) {
@@ -432,16 +423,22 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
             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;
+            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)) {
-                    nm_dhcp_option_add_option_utf8safe_escape(
-                        options,
-                        AF_INET,
-                        NM_DHCP_OPTION_DHCP4_PRIVATE_PROXY_AUTODISCOVERY,
-                        l_data,
-                        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;
             }
@@ -458,8 +455,12 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
             nm_dhcp_option_take_option(options, AF_INET, code, option_string);
         }
     }
-    NM_SET_OUT(out_options, g_steal_pointer(&options));
-    return g_steal_pointer(&ip4_config);
+
+    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);
 }
 
 /*****************************************************************************/
@@ -467,52 +468,46 @@ lease_to_ip4_config(NMDedupMultiIndex *multi_idx,
 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));
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    gs_unref_hashtable GHashTable *options  = NULL;
-    sd_dhcp_lease *                lease    = NULL;
-    GError *                       error    = NULL;
+    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_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return;
     }
 
     _LOGD("lease available");
 
-    ip4_config = 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,
-                                     nm_dhcp_client_get_route_table(NM_DHCP_CLIENT(self)),
-                                     nm_dhcp_client_get_route_metric(NM_DHCP_CLIENT(self)),
-                                     &options,
-                                     &error);
-    if (!ip4_config) {
+    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_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return;
     }
 
-    nm_dhcp_option_add_requests_to_options(options, AF_INET);
     dhcp_lease_save(lease, priv->lease_file);
 
     nm_dhcp_client_set_state(NM_DHCP_CLIENT(self),
                              extended ? NM_DHCP_STATE_EXTENDED : NM_DHCP_STATE_BOUND,
-                             NM_IP_CONFIG_CAST(ip4_config),
-                             options);
+                             l3cd);
 }
 
 static int
 dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
 {
-    NMDhcpSystemd *       self = NM_DHCP_SYSTEMD(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;
+    sd_dhcp_lease        *lease = NULL;
     struct in_addr        addr;
     int                   r;
 
@@ -522,10 +517,10 @@ dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
 
     switch (event) {
     case SD_DHCP_CLIENT_EVENT_EXPIRED:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_EXPIRE, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_EXPIRE, NULL);
         break;
     case SD_DHCP_CLIENT_EVENT_STOP:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL);
         break;
     case SD_DHCP_CLIENT_EVENT_RENEW:
     case SD_DHCP_CLIENT_EVENT_IP_CHANGE:
@@ -558,32 +553,35 @@ dhcp_event_cb(sd_dhcp_client *client, int event, gpointer user_data)
 }
 
 static gboolean
-ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
+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);
-    gs_free char *                                 lease_file = NULL;
-    GBytes *                                       hwaddr;
-    const uint8_t *                                hwaddr_arr;
+    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;
+    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);
@@ -600,7 +598,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         return FALSE;
     }
 
-    hwaddr = nm_dhcp_client_get_hw_addr(client);
+    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");
@@ -608,7 +606,8 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
     }
 
     bcast_hwaddr_arr = NULL;
-    if ((bcast_hwaddr = nm_dhcp_client_get_broadcast_hw_addr(NM_DHCP_CLIENT(self)))) {
+    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;
@@ -632,12 +631,12 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
 
     nm_dhcp_utils_get_leasefile_path(AF_INET,
                                      "internal",
-                                     nm_dhcp_client_get_iface(client),
-                                     nm_dhcp_client_get_uuid(client),
+                                     client_config->iface,
+                                     client_config->uuid,
                                      &lease_file);
 
-    if (last_ip4_address)
-        inet_pton(AF_INET, last_ip4_address, &last_addr);
+    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;
 
@@ -646,9 +645,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
             sd_dhcp_lease_get_address(lease, &last_addr);
     }
 
-    r = sd_dhcp_client_set_request_broadcast(sd_client,
-                                             NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(client),
-                                                          NM_DHCP_CLIENT_FLAGS_REQUEST_BROADCAST));
+    r = sd_dhcp_client_set_request_broadcast(sd_client, client_config->v4.request_broadcast);
     nm_assert(r >= 0);
 
     if (last_addr.s_addr) {
@@ -659,7 +656,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    client_id = nm_dhcp_client_get_client_id(client);
+    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;
@@ -694,7 +691,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    hostname = nm_dhcp_client_get_hostname(client);
+    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
@@ -707,7 +704,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    mud_url = nm_dhcp_client_get_mud_url(client);
+    mud_url = client_config->mud_url;
     if (mud_url) {
         r = sd_dhcp_client_set_mud_url(sd_client, mud_url);
         if (r < 0) {
@@ -716,7 +713,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         }
     }
 
-    vendor_class_identifier = nm_dhcp_client_get_vendor_class_identifier(client);
+    vendor_class_identifier = client_config->vendor_class_identifier;
     if (vendor_class_identifier) {
         const char *option_data;
         gsize       len;
@@ -745,7 +742,7 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
     g_free(priv->lease_file);
     priv->lease_file = g_steal_pointer(&lease_file);
 
-    nm_dhcp_client_set_client_id(client, client_id);
+    nm_dhcp_client_set_effective_client_id(client, client_id);
 
     r = sd_dhcp_client_start(priv->client4);
     if (r < 0) {
@@ -755,36 +752,37 @@ ip4_start(NMDhcpClient *client, const char *last_ip4_address, GError **error)
         return FALSE;
     }
 
-    nm_dhcp_client_start_timeout(client);
     return TRUE;
 }
 
-static NMIP6Config *
+static NML3ConfigData *
 lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
-                    const char *       iface,
+                    const char        *iface,
                     int                ifindex,
-                    sd_dhcp6_lease *   lease,
+                    sd_dhcp6_lease    *lease,
                     gboolean           info_only,
-                    GHashTable **      out_options,
                     gint32             ts,
-                    GError **          error)
+                    GError           **error)
 {
-    gs_unref_object NMIP6Config *ip6_config = NULL;
-    gs_unref_hashtable GHashTable *options  = NULL;
-    struct in6_addr                tmp_addr;
-    const struct in6_addr *        dns;
-    uint32_t                       lft_pref, lft_valid;
-    char                           addr_str[NM_UTILS_INET_ADDRSTRLEN];
-    char **                        domains;
-    const char *                   s;
-    nm_auto_free_gstring GString *str = NULL;
-    int                           num, i;
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd    = NULL;
+    gs_unref_hashtable GHashTable          *options = NULL;
+    struct in6_addr                         tmp_addr;
+    const struct in6_addr                  *dns;
+    uint32_t                                lft_pref, lft_valid;
+    char                                    addr_str[NM_UTILS_INET_ADDRSTRLEN];
+    char                                  **domains;
+    char                                  **ntp_fqdns;
+    const struct in6_addr                  *ntp_addrs;
+    const char                             *s;
+    nm_auto_free_gstring GString           *str               = NULL;
+    gboolean                                has_any_addresses = FALSE;
+    int                                     num, i;
 
     nm_assert(lease);
 
-    ip6_config = nm_ip6_config_new(multi_idx, ifindex);
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
 
-    options = out_options ? nm_dhcp_option_create_options_dict() : NULL;
+    options = nm_dhcp_option_create_options_dict();
 
     sd_dhcp6_lease_reset_address_iter(lease);
     nm_gstring_prepare(&str);
@@ -798,15 +796,19 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
             .addr_source = NM_IP_CONFIG_SOURCE_DHCP,
         };
 
-        nm_ip6_config_add_address(ip6_config, &address);
+        nm_l3_config_data_add_address_6(l3cd, &address);
 
         _nm_utils_inet6_ntop(&tmp_addr, addr_str);
         g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-    };
-    if (str->len)
+
+        has_any_addresses = TRUE;
+    }
+
+    if (str->len) {
         nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_NM_IP_ADDRESS, str->str);
+    }
 
-    if (!info_only && nm_ip6_config_get_num_addresses(ip6_config) == 0) {
+    if (!info_only && !has_any_addresses) {
         g_set_error_literal(error,
                             NM_MANAGER_ERROR,
                             NM_MANAGER_ERROR_FAILED,
@@ -820,7 +822,7 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
         for (i = 0; i < num; i++) {
             _nm_utils_inet6_ntop(&dns[i], addr_str);
             g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
-            nm_ip6_config_add_nameserver(ip6_config, &dns[i]);
+            nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &dns[i]);
         }
         nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_DNS_SERVERS, str->str);
     }
@@ -830,7 +832,7 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
         nm_gstring_prepare(&str);
         for (i = 0; i < num; i++) {
             g_string_append(nm_gstring_add_space_delimiter(str), domains[i]);
-            nm_ip6_config_add_search(ip6_config, domains[i]);
+            nm_l3_config_data_add_search(l3cd, AF_INET6, domains[i]);
         }
         nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, str->str);
     }
@@ -839,51 +841,72 @@ lease_to_ip6_config(NMDedupMultiIndex *multi_idx,
         nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_FQDN, s);
     }
 
-    NM_SET_OUT(out_options, g_steal_pointer(&options));
-    return g_steal_pointer(&ip6_config);
+    /* RFC 5908, section 4 states: "This option MUST include one, and only
+     * one, time source suboption." It is not clear why systemd chose to
+     * return array of addresses and FQDNs. Given there seem to be no
+     * technical obstacles to including multiple options, let's just
+     * pass on whatever systemd tells us.
+     */
+    nm_gstring_prepare(&str);
+    num = sd_dhcp6_lease_get_ntp_fqdn(lease, &ntp_fqdns);
+    if (num > 0) {
+        for (i = 0; i < num; i++) {
+            g_string_append(nm_gstring_add_space_delimiter(str), ntp_fqdns[i]);
+        }
+    }
+    num = sd_dhcp6_lease_get_ntp_addrs(lease, &ntp_addrs);
+    if (num > 0) {
+        for (i = 0; i < num; i++) {
+            _nm_utils_inet6_ntop(&ntp_addrs[i], addr_str);
+            g_string_append(nm_gstring_add_space_delimiter(str), addr_str);
+        }
+    }
+    if (str->len) {
+        nm_dhcp_option_add_option(options, AF_INET6, NM_DHCP_OPTION_DHCP6_NTP_SERVER, str->str);
+    }
+
+    nm_l3_config_data_set_dhcp_lease_from_options(l3cd, AF_INET6, g_steal_pointer(&options));
+
+    return g_steal_pointer(&l3cd);
 }
 
 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));
-    gs_unref_object NMIP6Config *ip6_config = NULL;
-    gs_unref_hashtable GHashTable *options  = NULL;
-    gs_free_error GError *error             = NULL;
-    NMPlatformIP6Address  prefix            = {0};
-    sd_dhcp6_lease *      lease             = NULL;
+    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;
+    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_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return;
     }
 
     _LOGD("lease available");
 
-    ip6_config =
-        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,
-                            NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(NM_DHCP_CLIENT(self)),
-                                         NM_DHCP_CLIENT_FLAGS_INFO_ONLY),
-                            &options,
-                            ts,
-                            &error);
-
-    if (!ip6_config) {
+    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);
+
+    if (!l3cd) {
         _LOGW("%s", error->message);
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_FAIL, NULL);
         return;
     }
 
-    nm_dhcp_client_set_state(NM_DHCP_CLIENT(self),
-                             NM_DHCP_STATE_BOUND,
-                             NM_IP_CONFIG_CAST(ip6_config),
-                             options);
+    nm_dhcp_client_set_state(NM_DHCP_CLIENT(self), NM_DHCP_STATE_BOUND, l3cd);
 
     sd_dhcp6_lease_reset_pd_prefix_iter(lease);
     while (!sd_dhcp6_lease_get_pd(lease,
@@ -899,7 +922,7 @@ bound6_handle(NMDhcpSystemd *self)
 static void
 dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data)
 {
-    NMDhcpSystemd *       self = NM_DHCP_SYSTEMD(user_data);
+    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(user_data);
     NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
 
     nm_assert(priv->client6 == client);
@@ -908,11 +931,11 @@ dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data)
 
     switch (event) {
     case SD_DHCP6_CLIENT_EVENT_RETRANS_MAX:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_TIMEOUT, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_TIMEOUT, NULL);
         break;
     case SD_DHCP6_CLIENT_EVENT_RESEND_EXPIRE:
     case SD_DHCP6_CLIENT_EVENT_STOP:
-        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL, NULL);
+        nm_dhcp_client_set_state(NM_DHCP_CLIENT(user_data), NM_DHCP_STATE_FAIL, NULL);
         break;
     case SD_DHCP6_CLIENT_EVENT_IP_ACQUIRE:
     case SD_DHCP6_CLIENT_EVENT_INFORMATION_REQUEST:
@@ -925,29 +948,29 @@ dhcp6_event_cb(sd_dhcp6_client *client, int event, gpointer user_data)
 }
 
 static gboolean
-ip6_start(NMDhcpClient *            client,
-          const struct in6_addr *   ll_addr,
-          NMSettingIP6ConfigPrivacy privacy,
-          guint                     needed_prefixes,
-          GError **                 error)
+ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
 {
-    NMDhcpSystemd *                                  self      = NM_DHCP_SYSTEMD(client);
-    NMDhcpSystemdPrivate *                           priv      = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
+    NMDhcpSystemd                                   *self      = NM_DHCP_SYSTEMD(client);
+    NMDhcpSystemdPrivate                            *priv      = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
     nm_auto(sd_dhcp6_client_unrefp) sd_dhcp6_client *sd_client = NULL;
-    const char *                                     hostname;
-    const char *                                     mud_url;
+    const NMDhcpClientConfig                        *client_config;
+    const char                                      *hostname;
+    const char                                      *mud_url;
     int                                              r, i;
-    const guint8 *                                   duid_arr;
+    const guint8                                    *duid_arr;
     gsize                                            duid_len;
-    GBytes *                                         duid;
+    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);
+
     /* TODO: honor nm_dhcp_client_get_anycast_address() */
 
-    if (!(duid = nm_dhcp_client_get_client_id(client))
-        || !(duid_arr = g_bytes_get_data(duid, &duid_len)) || duid_len < 2) {
+    duid = nm_dhcp_client_get_effective_client_id(client);
+    if (!duid || !(duid_arr = g_bytes_get_data(duid, &duid_len)) || duid_len < 2) {
         nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing DUID");
         g_return_val_if_reached(FALSE);
     }
@@ -960,13 +983,13 @@ ip6_start(NMDhcpClient *            client,
 
     _LOGT("dhcp-client6: set %p", sd_client);
 
-    if (NM_FLAGS_HAS(nm_dhcp_client_get_client_flags(client), NM_DHCP_CLIENT_FLAGS_INFO_ONLY)) {
+    if (client_config->v6.info_only) {
         sd_dhcp6_client_set_address_request(sd_client, 0);
-        if (needed_prefixes == 0)
+        if (client_config->v6.needed_prefixes == 0)
             sd_dhcp6_client_set_information_request(sd_client, 1);
     }
 
-    r = sd_dhcp6_client_set_iaid(sd_client, nm_dhcp_client_get_iaid(client));
+    r = sd_dhcp6_client_set_iaid(sd_client, client_config->v6.iaid);
     if (r < 0) {
         nm_utils_error_set_errno(error, r, "failed to set IAID: %s");
         return FALSE;
@@ -1002,7 +1025,7 @@ ip6_start(NMDhcpClient *            client,
         }
     }
 
-    mud_url = nm_dhcp_client_get_mud_url(client);
+    mud_url = client_config->mud_url;
     if (mud_url) {
         r = sd_dhcp6_client_set_request_mud_url(sd_client, mud_url);
         if (r < 0) {
@@ -1011,16 +1034,19 @@ ip6_start(NMDhcpClient *            client,
         }
     }
 
-    if (needed_prefixes > 0) {
-        if (needed_prefixes > 1)
+    prefix_delegation = FALSE;
+    if (client_config->v6.needed_prefixes > 0) {
+        if (client_config->v6.needed_prefixes > 1) {
+            /* FIXME: systemd-networkd API only allows to request a
+             * single prefix */
             _LOGW("dhcp-client6: only one prefix request is supported");
-        /* FIXME: systemd-networkd API only allows to request a
-         * single prefix */
-        r = sd_dhcp6_client_set_prefix_delegation(sd_client, TRUE);
-        if (r < 0) {
-            nm_utils_error_set_errno(error, r, "failed to enable prefix delegation: %s");
-            return FALSE;
         }
+        prefix_delegation = TRUE;
+    }
+    r = sd_dhcp6_client_set_prefix_delegation(sd_client, prefix_delegation);
+    if (r < 0) {
+        nm_utils_error_set_errno(error, r, "failed to enable prefix delegation: %s");
+        return FALSE;
     }
 
     r = sd_dhcp6_client_set_local_address(sd_client, ll_addr);
@@ -1029,7 +1055,7 @@ ip6_start(NMDhcpClient *            client,
         return FALSE;
     }
 
-    hostname = nm_dhcp_client_get_hostname(client);
+    hostname = client_config->hostname;
     r        = sd_dhcp6_client_set_fqdn(sd_client, hostname);
     if (r < 0) {
         nm_utils_error_set_errno(error, r, "failed to set DHCP hostname: %s");
@@ -1052,14 +1078,13 @@ ip6_start(NMDhcpClient *            client,
         return FALSE;
     }
 
-    nm_dhcp_client_start_timeout(client);
     return TRUE;
 }
 
 static void
 stop(NMDhcpClient *client, gboolean release)
 {
-    NMDhcpSystemd *       self = NM_DHCP_SYSTEMD(client);
+    NMDhcpSystemd        *self = NM_DHCP_SYSTEMD(client);
     NMDhcpSystemdPrivate *priv = NM_DHCP_SYSTEMD_GET_PRIVATE(self);
     int                   r    = 0;
 
@@ -1113,7 +1138,7 @@ static void
 nm_dhcp_systemd_class_init(NMDhcpSystemdClass *sdhcp_class)
 {
     NMDhcpClientClass *client_class = NM_DHCP_CLIENT_CLASS(sdhcp_class);
-    GObjectClass *     object_class = G_OBJECT_CLASS(sdhcp_class);
+    GObjectClass      *object_class = G_OBJECT_CLASS(sdhcp_class);
 
     object_class->dispose = dispose;
 
diff --git a/src/core/dhcp/nm-dhcp-utils.c b/src/core/dhcp/nm-dhcp-utils.c
index 3cc6987a..4a138086 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-l3-config-data.h"
 #include "nm-utils.h"
 #include "nm-config.h"
 #include "NetworkManagerUtils.h"
@@ -24,15 +25,13 @@
 /*****************************************************************************/
 
 static gboolean
-ip4_process_dhcpcd_rfc3442_routes(const char * iface,
-                                  const char * str,
-                                  guint32      route_table,
-                                  guint32      route_metric,
-                                  NMIP4Config *ip4_config,
-                                  guint32 *    gwaddr)
+ip4_process_dhcpcd_rfc3442_routes(const char     *iface,
+                                  const char     *str,
+                                  NML3ConfigData *l3cd,
+                                  guint32        *gwaddr)
 {
     gs_free const char **routes = NULL;
-    const char **        r;
+    const char         **r;
     gboolean             have_routes = FALSE;
 
     routes = nm_strsplit_set(str, " ");
@@ -45,10 +44,9 @@ ip4_process_dhcpcd_rfc3442_routes(const char * iface,
     }
 
     for (r = routes; *r; r += 2) {
-        char *             slash;
-        NMPlatformIP4Route route;
-        int                rt_cidr = 32;
-        guint32            rt_addr, rt_route;
+        char   *slash;
+        int     rt_cidr = 32;
+        guint32 rt_addr, rt_route;
 
         slash = strchr(*r, '/');
         if (slash) {
@@ -89,14 +87,18 @@ ip4_process_dhcpcd_rfc3442_routes(const char * iface,
                    *r,
                    rt_cidr,
                    *(r + 1));
-            memset(&route, 0, sizeof(route));
-            route.network       = nm_utils_ip4_address_clear_host_address(rt_addr, rt_cidr);
-            route.plen          = rt_cidr;
-            route.gateway       = rt_route;
-            route.rt_source     = NM_IP_CONFIG_SOURCE_DHCP;
-            route.metric        = route_metric;
-            route.table_coerced = nm_platform_route_table_coerce(route_table);
-            nm_ip4_config_add_route(ip4_config, &route, NULL);
+
+            nm_l3_config_data_add_route_4(
+                l3cd,
+                &((const NMPlatformIP4Route){
+                    .network    = nm_utils_ip4_address_clear_host_address(rt_addr, rt_cidr),
+                    .plen       = rt_cidr,
+                    .gateway    = rt_route,
+                    .rt_source  = NM_IP_CONFIG_SOURCE_DHCP,
+                    .metric_any = TRUE,
+                    .table_any  = TRUE,
+
+                }));
         }
     }
 
@@ -107,7 +109,7 @@ static gboolean
 process_dhclient_rfc3442_route(const char *const **p_octets, NMPlatformIP4Route *route)
 {
     const char *const *o        = *p_octets;
-    gs_free char *     next_hop = NULL;
+    gs_free char      *next_hop = NULL;
     int                addr_len;
     int                v_plen;
     in_addr_t          tmp_addr;
@@ -125,7 +127,7 @@ process_dhclient_rfc3442_route(const char *const **p_octets, NMPlatformIP4Route
         return FALSE;
 
     if (v_plen > 0) {
-        const char *  addr[4]  = {"0", "0", "0", "0"};
+        const char   *addr[4]  = {"0", "0", "0", "0"};
         gs_free char *str_addr = NULL;
         int           i;
 
@@ -153,15 +155,13 @@ process_dhclient_rfc3442_route(const char *const **p_octets, NMPlatformIP4Route
 }
 
 static gboolean
-ip4_process_dhclient_rfc3442_routes(const char * iface,
-                                    const char * str,
-                                    guint32      route_table,
-                                    guint32      route_metric,
-                                    NMIP4Config *ip4_config,
-                                    guint32 *    gwaddr)
+ip4_process_dhclient_rfc3442_routes(const char     *iface,
+                                    const char     *str,
+                                    NML3ConfigData *l3cd,
+                                    guint32        *gwaddr)
 {
     gs_free const char **octets = NULL;
-    const char *const *  o;
+    const char *const   *o;
     gboolean             have_routes = FALSE;
 
     octets = nm_strsplit_set_with_empty(str, " .");
@@ -189,9 +189,12 @@ ip4_process_dhclient_rfc3442_routes(const char * iface,
 
             /* normal route */
             route.rt_source     = NM_IP_CONFIG_SOURCE_DHCP;
-            route.metric        = route_metric;
-            route.table_coerced = nm_platform_route_table_coerce(route_table);
-            nm_ip4_config_add_route(ip4_config, &route, NULL);
+            route.table_any     = TRUE;
+            route.table_coerced = 0;
+            route.metric_any    = TRUE;
+            route.metric        = 0;
+
+            nm_l3_config_data_add_route_4(l3cd, &route);
 
             _LOG2I(LOGD_DHCP4,
                    iface,
@@ -206,17 +209,15 @@ ip4_process_dhclient_rfc3442_routes(const char * iface,
 }
 
 static gboolean
-ip4_process_classless_routes(const char * iface,
-                             GHashTable * options,
-                             guint32      route_table,
-                             guint32      route_metric,
-                             NMIP4Config *ip4_config,
-                             guint32 *    gwaddr)
+ip4_process_classless_routes(const char     *iface,
+                             GHashTable     *options,
+                             NML3ConfigData *l3cd,
+                             guint32        *gwaddr)
 {
     const char *str, *p;
 
     g_return_val_if_fail(options != NULL, FALSE);
-    g_return_val_if_fail(ip4_config != NULL, FALSE);
+    g_return_val_if_fail(l3cd != NULL, FALSE);
 
     *gwaddr = 0;
 
@@ -251,7 +252,7 @@ ip4_process_classless_routes(const char * iface,
     if (!str)
         str = g_hash_table_lookup(options, "ms_classless_static_routes");
 
-    if (!str || !strlen(str))
+    if (nm_str_is_empty(str))
         return FALSE;
 
     p = str;
@@ -265,32 +266,18 @@ ip4_process_classless_routes(const char * iface,
 
     if (strchr(str, '/')) {
         /* dhcpcd format */
-        return ip4_process_dhcpcd_rfc3442_routes(iface,
-                                                 str,
-                                                 route_table,
-                                                 route_metric,
-                                                 ip4_config,
-                                                 gwaddr);
+        return ip4_process_dhcpcd_rfc3442_routes(iface, str, l3cd, gwaddr);
     }
 
-    return ip4_process_dhclient_rfc3442_routes(iface,
-                                               str,
-                                               route_table,
-                                               route_metric,
-                                               ip4_config,
-                                               gwaddr);
+    return ip4_process_dhclient_rfc3442_routes(iface, str, l3cd, gwaddr);
 }
 
 static void
-process_classful_routes(const char * iface,
-                        GHashTable * options,
-                        guint32      route_table,
-                        guint32      route_metric,
-                        NMIP4Config *ip4_config)
+process_classful_routes(const char *iface, GHashTable *options, NML3ConfigData *l3cd)
 {
     gs_free const char **searches = NULL;
-    const char **        s;
-    const char *         str;
+    const char         **s;
+    const char          *str;
 
     str = g_hash_table_lookup(options, "static_routes");
     if (!str)
@@ -320,8 +307,10 @@ process_classful_routes(const char * iface,
 
         // FIXME: ensure the IP address and route are sane
 
-        memset(&route, 0, sizeof(route));
-        route.network = rt_addr;
+        route = (NMPlatformIP4Route){
+            .network = rt_addr,
+        };
+
         /* RFC 2132, updated by RFC 3442:
          * The Static Routes option (option 33) does not provide a subnet mask
          * for each route - it is assumed that the subnet mask is implicit in
@@ -333,12 +322,15 @@ process_classful_routes(const char * iface,
         }
         route.gateway       = rt_route;
         route.rt_source     = NM_IP_CONFIG_SOURCE_DHCP;
-        route.metric        = route_metric;
-        route.table_coerced = nm_platform_route_table_coerce(route_table);
+        route.table_any     = TRUE;
+        route.table_coerced = 0;
+        route.metric_any    = TRUE;
+        route.metric        = 0;
 
         route.network = nm_utils_ip4_address_clear_host_address(route.network, route.plen);
 
-        nm_ip4_config_add_route(ip4_config, &route, NULL);
+        nm_l3_config_data_add_route_4(l3cd, &route);
+
         _LOG2I(LOGD_DHCP,
                iface,
                "  static route %s",
@@ -347,16 +339,16 @@ process_classful_routes(const char * iface,
 }
 
 static void
-process_domain_search(const char *iface, const char *str, GFunc add_func, gpointer user_data)
+process_domain_search(int addr_family, const char *iface, const char *str, NML3ConfigData *l3cd)
 {
     gs_free const char **searches  = NULL;
-    gs_free char *       unescaped = NULL;
-    const char **        s;
-    char *               p;
+    gs_free char        *unescaped = NULL;
+    const char         **s;
+    char                *p;
     int                  i;
 
     g_return_if_fail(str != NULL);
-    g_return_if_fail(add_func != NULL);
+    nm_assert(l3cd);
 
     unescaped = g_strdup(str);
 
@@ -379,46 +371,43 @@ process_domain_search(const char *iface, const char *str, GFunc add_func, gpoint
     searches = nm_strsplit_set(unescaped, " ");
     for (s = searches; searches && *s; s++) {
         _LOG2I(LOGD_DHCP, iface, "  domain search '%s'", *s);
-        add_func((gpointer) *s, user_data);
+        nm_l3_config_data_add_search(l3cd, addr_family, *s);
     }
 }
 
-static void
-ip4_add_domain_search(gpointer data, gpointer user_data)
-{
-    nm_ip4_config_add_search(NM_IP4_CONFIG(user_data), (const char *) data);
-}
-
-NMIP4Config *
+NML3ConfigData *
 nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
                                       int                ifindex,
-                                      const char *       iface,
-                                      GHashTable *       options,
-                                      guint32            route_table,
-                                      guint32            route_metric)
+                                      const char        *iface,
+                                      GHashTable        *options)
 {
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    guint32                      tmp_addr;
-    in_addr_t                    addr;
-    NMPlatformIP4Address         address;
-    char *                       str         = NULL;
-    gboolean                     gateway_has = FALSE;
-    guint32                      gateway     = 0;
-    guint8                       plen        = 0;
-    char                         sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
+    guint32                                 tmp_addr;
+    in_addr_t                               addr;
+    NMPlatformIP4Address                    address;
+    char                                   *str         = NULL;
+    gboolean                                gateway_has = FALSE;
+    guint32                                 gateway     = 0;
+    guint8                                  plen        = 0;
+    char                                    sbuf[NM_UTILS_INET_ADDRSTRLEN];
+    guint32                                 now;
 
     g_return_val_if_fail(options != NULL, NULL);
 
-    ip4_config = nm_ip4_config_new(multi_idx, ifindex);
-    memset(&address, 0, sizeof(address));
-    address.timestamp = nm_utils_get_monotonic_timestamp_sec();
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
+
+    now = nm_utils_get_monotonic_timestamp_sec();
+
+    address = (NMPlatformIP4Address){
+        .timestamp = now,
+    };
 
     str = g_hash_table_lookup(options, "ip_address");
-    if (str && (inet_pton(AF_INET, str, &addr) > 0))
-        _LOG2I(LOGD_DHCP4, iface, "  address %s", str);
-    else
+    if (!str || !nm_utils_parse_inaddr_bin(AF_INET, str, NULL, &addr))
         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);
@@ -433,13 +422,8 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     /* Routes: if the server returns classless static routes, we MUST ignore
      * the 'static_routes' option.
      */
-    if (!ip4_process_classless_routes(iface,
-                                      options,
-                                      route_table,
-                                      route_metric,
-                                      ip4_config,
-                                      &gateway))
-        process_classful_routes(iface, options, route_table, route_metric, ip4_config);
+    if (!ip4_process_classless_routes(iface, options, l3cd, &gateway))
+        process_classful_routes(iface, options, l3cd);
 
     if (gateway) {
         _LOG2I(LOGD_DHCP4, iface, "  gateway %s", _nm_utils_inet4_ntop(gateway, sbuf));
@@ -451,9 +435,9 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
         str = g_hash_table_lookup(options, "routers");
         if (str) {
             gs_free const char **routers = nm_strsplit_set(str, " ");
-            const char **        s;
+            const char         **s;
 
-            for (s = routers; routers && *s; s++) {
+            for (s = routers; *s; s++) {
                 /* FIXME: how to handle multiple routers? */
                 if (inet_pton(AF_INET, *s, &gateway) > 0) {
                     _LOG2I(LOGD_DHCP4, iface, "  gateway %s", *s);
@@ -469,11 +453,13 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
         const NMPlatformIP4Route r = {
             .rt_source     = NM_IP_CONFIG_SOURCE_DHCP,
             .gateway       = gateway,
-            .table_coerced = nm_platform_route_table_coerce(route_table),
-            .metric        = route_metric,
+            .table_any     = TRUE,
+            .table_coerced = 0,
+            .metric_any    = TRUE,
+            .metric        = 0,
         };
 
-        nm_ip4_config_add_route(ip4_config, &r, NULL);
+        nm_l3_config_data_add_route_4(l3cd, &r);
     }
 
     str = g_hash_table_lookup(options, "dhcp_lease_time");
@@ -483,7 +469,8 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     }
 
     address.addr_source = NM_IP_CONFIG_SOURCE_DHCP;
-    nm_ip4_config_add_address(ip4_config, &address);
+
+    nm_l3_config_data_add_address_4(l3cd, &address);
 
     str = g_hash_table_lookup(options, "host_name");
     if (str)
@@ -492,12 +479,12 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     str = g_hash_table_lookup(options, "domain_name_servers");
     if (str) {
         gs_free const char **dns = nm_strsplit_set(str, " ");
-        const char **        s;
+        const char         **s;
 
         for (s = dns; dns && *s; s++) {
             if (inet_pton(AF_INET, *s, &tmp_addr) > 0) {
                 if (tmp_addr) {
-                    nm_ip4_config_add_nameserver(ip4_config, tmp_addr);
+                    nm_l3_config_data_add_nameserver(l3cd, AF_INET, &tmp_addr);
                     _LOG2I(LOGD_DHCP4, iface, "  nameserver '%s'", *s);
                 }
             } else
@@ -508,27 +495,27 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     str = g_hash_table_lookup(options, "domain_name");
     if (str) {
         gs_free const char **domains = nm_strsplit_set(str, " ");
-        const char **        s;
+        const char         **s;
 
         for (s = domains; domains && *s; s++) {
             _LOG2I(LOGD_DHCP4, iface, "  domain name '%s'", *s);
-            nm_ip4_config_add_domain(ip4_config, *s);
+            nm_l3_config_data_add_domain(l3cd, AF_INET, *s);
         }
     }
 
     str = g_hash_table_lookup(options, "domain_search");
     if (str)
-        process_domain_search(iface, str, ip4_add_domain_search, ip4_config);
+        process_domain_search(AF_INET, iface, str, l3cd);
 
     str = g_hash_table_lookup(options, "netbios_name_servers");
     if (str) {
         gs_free const char **nbns = nm_strsplit_set(str, " ");
-        const char **        s;
+        const char         **s;
 
         for (s = nbns; nbns && *s; s++) {
             if (inet_pton(AF_INET, *s, &tmp_addr) > 0) {
                 if (tmp_addr) {
-                    nm_ip4_config_add_wins(ip4_config, tmp_addr);
+                    nm_l3_config_data_add_wins(l3cd, tmp_addr);
                     _LOG2I(LOGD_DHCP4, iface, "  wins '%s'", *s);
                 }
             } else
@@ -546,24 +533,24 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
             return NULL;
 
         if (int_mtu > 576)
-            nm_ip4_config_set_mtu(ip4_config, int_mtu, NM_IP_CONFIG_SOURCE_DHCP);
+            nm_l3_config_data_set_mtu(l3cd, int_mtu);
     }
 
     str = g_hash_table_lookup(options, "nis_domain");
     if (str) {
         _LOG2I(LOGD_DHCP4, iface, "  NIS domain '%s'", str);
-        nm_ip4_config_set_nis_domain(ip4_config, str);
+        nm_l3_config_data_add_domain(l3cd, AF_INET, str);
     }
 
     str = g_hash_table_lookup(options, "nis_servers");
     if (str) {
         gs_free const char **nis = nm_strsplit_set(str, " ");
-        const char **        s;
+        const char         **s;
 
         for (s = nis; nis && *s; s++) {
             if (inet_pton(AF_INET, *s, &tmp_addr) > 0) {
                 if (tmp_addr) {
-                    nm_ip4_config_add_nis_server(ip4_config, tmp_addr);
+                    nm_l3_config_data_add_nis_server(l3cd, tmp_addr);
                     _LOG2I(LOGD_DHCP4, iface, "  nis '%s'", *s);
                 }
             } else
@@ -572,28 +559,29 @@ nm_dhcp_utils_ip4_config_from_options(NMDedupMultiIndex *multi_idx,
     }
 
     str = g_hash_table_lookup(options, "vendor_encapsulated_options");
-    nm_ip4_config_set_metered(ip4_config, str && strstr(str, "ANDROID_METERED"));
+    if (str && strstr(str, "ANDROID_METERED"))
+        nm_l3_config_data_set_metered(l3cd, TRUE);
 
-    return g_steal_pointer(&ip4_config);
+    str = g_hash_table_lookup(options, "wpad");
+    if (str) {
+        nm_l3_config_data_set_proxy_method(l3cd, NM_PROXY_CONFIG_METHOD_AUTO);
+        nm_l3_config_data_set_proxy_pac_url(l3cd, str);
+    }
+
+    return g_steal_pointer(&l3cd);
 }
 
 /*****************************************************************************/
 
-static void
-ip6_add_domain_search(gpointer data, gpointer user_data)
-{
-    nm_ip6_config_add_search(NM_IP6_CONFIG(user_data), (const char *) data);
-}
-
 NMPlatformIP6Address
 nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options)
 {
-    gs_strfreev char **  split_addr = NULL;
+    gs_strfreev char   **split_addr = NULL;
     NMPlatformIP6Address address    = {
         0,
     };
     struct in6_addr tmp_addr;
-    char *          str = NULL;
+    char           *str = NULL;
     int             prefix;
 
     g_return_val_if_fail(options != NULL, address);
@@ -635,25 +623,29 @@ nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options)
     return address;
 }
 
-NMIP6Config *
+NML3ConfigData *
 nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx,
                                       int                ifindex,
-                                      const char *       iface,
-                                      GHashTable *       options,
+                                      const char        *iface,
+                                      GHashTable        *options,
                                       gboolean           info_only)
 {
-    gs_unref_object NMIP6Config *ip6_config = NULL;
-    struct in6_addr              tmp_addr;
-    NMPlatformIP6Address         address;
-    char *                       str = NULL;
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd = NULL;
+    struct in6_addr                         tmp_addr;
+    NMPlatformIP6Address                    address;
+    char                                   *str = NULL;
+    guint32                                 now;
 
     g_return_val_if_fail(options != NULL, NULL);
 
-    memset(&address, 0, sizeof(address));
-    address.plen      = 128;
-    address.timestamp = nm_utils_get_monotonic_timestamp_sec();
+    now = nm_utils_get_monotonic_timestamp_sec();
+
+    address = (NMPlatformIP6Address){
+        .plen      = 128,
+        .timestamp = now,
+    };
 
-    ip6_config = nm_ip6_config_new(multi_idx, ifindex);
+    l3cd = nm_l3_config_data_new(multi_idx, ifindex, NM_IP_CONFIG_SOURCE_DHCP);
 
     str = g_hash_table_lookup(options, "max_life");
     if (str) {
@@ -676,7 +668,7 @@ nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx,
 
         address.address     = tmp_addr;
         address.addr_source = NM_IP_CONFIG_SOURCE_DHCP;
-        nm_ip6_config_add_address(ip6_config, &address);
+        nm_l3_config_data_add_address_6(l3cd, &address);
         _LOG2I(LOGD_DHCP6, iface, "  address %s", str);
     } else if (info_only == FALSE) {
         /* No address in Managed mode is a hard error */
@@ -690,12 +682,12 @@ nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx,
     str = g_hash_table_lookup(options, "dhcp6_name_servers");
     if (str) {
         gs_free const char **dns = nm_strsplit_set(str, " ");
-        const char **        s;
+        const char         **s;
 
         for (s = dns; dns && *s; s++) {
             if (inet_pton(AF_INET6, *s, &tmp_addr) > 0) {
                 if (!IN6_IS_ADDR_UNSPECIFIED(&tmp_addr)) {
-                    nm_ip6_config_add_nameserver(ip6_config, &tmp_addr);
+                    nm_l3_config_data_add_nameserver(l3cd, AF_INET6, &tmp_addr);
                     _LOG2I(LOGD_DHCP6, iface, "  nameserver '%s'", *s);
                 }
             } else
@@ -705,9 +697,9 @@ nm_dhcp_utils_ip6_config_from_options(NMDedupMultiIndex *multi_idx,
 
     str = g_hash_table_lookup(options, "dhcp6_domain_search");
     if (str)
-        process_domain_search(iface, str, ip6_add_domain_search, ip6_config);
+        process_domain_search(AF_INET6, iface, str, l3cd);
 
-    return g_steal_pointer(&ip6_config);
+    return g_steal_pointer(&l3cd);
 }
 
 char *
@@ -739,7 +731,7 @@ nm_dhcp_utils_client_id_string_to_bytes(const char *client_id)
 {
     GBytes *bytes = NULL;
     guint   len;
-    char *  c;
+    char   *c;
 
     g_return_val_if_fail(client_id && client_id[0], NULL);
 
@@ -784,7 +776,7 @@ nm_dhcp_utils_get_leasefile_path(int         addr_family,
                                  const char *plugin_name,
                                  const char *iface,
                                  const char *uuid,
-                                 char **     out_leasefile_path)
+                                 char      **out_leasefile_path)
 {
     gs_free char *rundir_path   = NULL;
     gs_free char *statedir_path = NULL;
@@ -838,6 +830,65 @@ nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease)
     return g_strdup_printf("%s|%s", iaid, start);
 }
 
+gboolean
+nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
+                                    const NML3ConfigData  *l3cd_new,
+                                    const NML3ConfigData **out_l3cd_merged)
+{
+    nm_auto_unref_l3cd_init NML3ConfigData *l3cd_merged = NULL;
+    const NMPlatformIP6Address             *addr;
+    NMDhcpLease                            *lease_old;
+    NMDhcpLease                            *lease_new;
+    NMDedupMultiIter                        iter;
+    const char                             *start;
+    const char                             *iaid;
+
+    nm_assert(out_l3cd_merged);
+    nm_assert(!*out_l3cd_merged);
+
+    if (!l3cd_old)
+        return FALSE;
+    if (!l3cd_new)
+        return FALSE;
+
+    lease_new = nm_l3_config_data_get_dhcp_lease(l3cd_new, AF_INET6);
+    if (!lease_new)
+        return FALSE;
+
+    lease_old = nm_l3_config_data_get_dhcp_lease(l3cd_old, AF_INET6);
+    if (!lease_old)
+        return FALSE;
+
+    start = nm_dhcp_lease_lookup_option(lease_new, "life_starts");
+    if (!start)
+        return FALSE;
+    iaid = nm_dhcp_lease_lookup_option(lease_new, "iaid");
+    if (!iaid)
+        return FALSE;
+
+    if (!nm_streq0(start, nm_dhcp_lease_lookup_option(lease_old, "life_starts")))
+        return FALSE;
+    if (!nm_streq0(iaid, nm_dhcp_lease_lookup_option(lease_old, "iaid")))
+        return FALSE;
+
+    /* If the server sends multiple IPv6 addresses, we receive a state
+     * changed event for each of them. Use the event ID to merge IPv6
+     * addresses from the same transaction into a single configuration.
+     **/
+
+    l3cd_merged = nm_l3_config_data_new_clone(l3cd_old, -1);
+
+    nm_l3_config_data_iter_ip6_address_for_each (&iter, l3cd_new, &addr)
+        nm_l3_config_data_add_address_6(l3cd_merged, addr);
+
+    /* FIXME(l3cfg): Note that we keep the original NMDhcpLease. All we take from the new lease are the
+     * addresses. Maybe this is not right and we should merge the leases too?? */
+    nm_l3_config_data_set_dhcp_lease(l3cd_merged, AF_INET6, lease_old);
+
+    *out_l3cd_merged = nm_l3_config_data_ref_and_seal(g_steal_pointer(&l3cd_merged));
+    return TRUE;
+}
+
 /*****************************************************************************/
 
 gboolean
@@ -922,7 +973,7 @@ gboolean
 nm_dhcp_lease_data_parse_domain(const guint8 *data, gsize n_data, char **out_val)
 {
     gs_free char *str1_free = NULL;
-    const char *  str1;
+    const char   *str1;
     gs_free char *s = NULL;
 
     /* this is mostly the same as systemd's lease_parse_domain(). */
@@ -959,7 +1010,11 @@ nm_dhcp_lease_data_parse_in_addr(const guint8 *data, gsize n_data, in_addr_t *ou
      * - option 28, https://tools.ietf.org/html/rfc2132#section-5.3
      */
 
-    if (n_data != 4)
+    /* Some DHCP servers send duplicate options, and we concatenate them
+     * 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)
         return FALSE;
 
     *out_val = unaligned_read_ne32(data);
@@ -1000,17 +1055,17 @@ lease_option_print_label(NMStrBuf *sbuf, size_t n_label, const uint8_t **datap,
 }
 
 static char *
-lease_option_print_domain_name(const uint8_t * cache,
-                               size_t *        n_cachep,
+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)
 {
     nm_auto_str_buf NMStrBuf sbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_40, FALSE);
-    const uint8_t *          domain;
+    const uint8_t           *domain;
     size_t                   n_domain;
     size_t                   n_cache   = *n_cachep;
-    const uint8_t **         domainp   = datap;
-    size_t *                 n_domainp = n_datap;
+    const uint8_t          **domainp   = datap;
+    size_t                  *n_domainp = n_datap;
     gboolean                 first     = TRUE;
     uint8_t                  c;
 
@@ -1096,7 +1151,7 @@ lease_option_print_domain_name(const uint8_t * cache,
 char **
 nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data)
 {
-    GPtrArray *   array   = NULL;
+    GPtrArray    *array   = NULL;
     const guint8 *cache   = data;
     gsize         n_cache = 0;
 
diff --git a/src/core/dhcp/nm-dhcp-utils.h b/src/core/dhcp/nm-dhcp-utils.h
index 69715f90..bef50c52 100644
--- a/src/core/dhcp/nm-dhcp-utils.h
+++ b/src/core/dhcp/nm-dhcp-utils.h
@@ -8,21 +8,18 @@
 
 #include <stdlib.h>
 
-#include "nm-ip4-config.h"
-#include "nm-ip6-config.h"
-
-NMIP4Config *nm_dhcp_utils_ip4_config_from_options(struct _NMDedupMultiIndex *multi_idx,
-                                                   int                        ifindex,
-                                                   const char *               iface,
-                                                   GHashTable *               options,
-                                                   guint32                    route_table,
-                                                   guint32                    route_metric);
-
-NMIP6Config *nm_dhcp_utils_ip6_config_from_options(struct _NMDedupMultiIndex *multi_idx,
-                                                   int                        ifindex,
-                                                   const char *               iface,
-                                                   GHashTable *               options,
-                                                   gboolean                   info_only);
+#include "nm-l3-config-data.h"
+
+NML3ConfigData *nm_dhcp_utils_ip4_config_from_options(struct _NMDedupMultiIndex *multi_idx,
+                                                      int                        ifindex,
+                                                      const char                *iface,
+                                                      GHashTable                *options);
+
+NML3ConfigData *nm_dhcp_utils_ip6_config_from_options(struct _NMDedupMultiIndex *multi_idx,
+                                                      int                        ifindex,
+                                                      const char                *iface,
+                                                      GHashTable                *options,
+                                                      gboolean                   info_only);
 
 NMPlatformIP6Address nm_dhcp_utils_ip6_prefix_from_options(GHashTable *options);
 
@@ -34,10 +31,14 @@ gboolean nm_dhcp_utils_get_leasefile_path(int         addr_family,
                                           const char *plugin_name,
                                           const char *iface,
                                           const char *uuid,
-                                          char **     out_leasefile_path);
+                                          char      **out_leasefile_path);
 
 char *nm_dhcp_utils_get_dhcp6_event_id(GHashTable *lease);
 
+gboolean nm_dhcp_utils_merge_new_dhcp6_lease(const NML3ConfigData  *l3cd_old,
+                                             const NML3ConfigData  *l3cd_new,
+                                             const NML3ConfigData **out_l3cd_merged);
+
 /*****************************************************************************/
 
 static inline gboolean
@@ -65,6 +66,6 @@ gboolean nm_dhcp_lease_data_parse_mtu(const guint8 *data, gsize n_data, guint16
 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);
+char   **nm_dhcp_lease_data_parse_search_list(const guint8 *data, gsize n_data);
 
 #endif /* __NETWORKMANAGER_DHCP_UTILS_H__ */
diff --git a/src/core/dhcp/tests/test-dhcp-dhclient.c b/src/core/dhcp/tests/test-dhcp-dhclient.c
index 9c6865cc..aaa0dbc3 100644
--- a/src/core/dhcp/tests/test-dhcp-dhclient.c
+++ b/src/core/dhcp/tests/test-dhcp-dhclient.c
@@ -15,7 +15,6 @@
 #include "dhcp/nm-dhcp-dhclient-utils.h"
 #include "dhcp/nm-dhcp-utils.h"
 #include "nm-utils.h"
-#include "nm-ip4-config.h"
 #include "libnm-platform/nm-platform.h"
 
 #include "nm-test-utils-core.h"
@@ -24,18 +23,18 @@
 #define TEST_MUDURL "https://example.com/mud.json"
 
 static void
-test_config(const char *        orig,
-            const char *        expected,
+test_config(const char         *orig,
+            const char         *expected,
             int                 addr_family,
-            const char *        hostname,
+            const char         *hostname,
             guint32             timeout,
             gboolean            use_fqdn,
             NMDhcpHostnameFlags hostname_flags,
-            const char *        dhcp_client_id,
-            GBytes *            expected_new_client_id,
-            const char *        iface,
-            const char *        anycast_addr,
-            const char *        mud_url)
+            const char         *dhcp_client_id,
+            GBytes             *expected_new_client_id,
+            const char         *iface,
+            const char         *anycast_addr,
+            const char         *mud_url)
 {
     gs_free char *new                    = NULL;
     gs_unref_bytes GBytes *client_id     = NULL;
@@ -826,7 +825,7 @@ test_one_duid(const char *escaped, const guint8 *unescaped, guint len)
 {
     gs_unref_bytes GBytes *t1 = NULL;
     gs_unref_bytes GBytes *t2 = NULL;
-    gs_free char *         w  = NULL;
+    gs_free char          *w  = NULL;
 
     t1 = nm_dhcp_dhclient_unescape_duid(escaped);
     g_assert(t1);
@@ -874,7 +873,7 @@ test_read_duid_from_leasefile(void)
     const guint8 expected[] =
         {0x00, 0x01, 0x00, 0x01, 0x18, 0x79, 0xa6, 0x13, 0x60, 0x67, 0x20, 0xec, 0x4c, 0x70};
     gs_unref_bytes GBytes *duid  = NULL;
-    GError *               error = NULL;
+    GError                *error = NULL;
 
     duid = nm_dhcp_dhclient_read_duid(TEST_DIR "/test-dhclient-duid.leases", &error);
     nmtst_assert_success(duid, error);
@@ -899,7 +898,7 @@ static void
 _save_duid(const char *path, const guint8 *duid_bin, gsize duid_len)
 {
     gs_unref_bytes GBytes *duid  = NULL;
-    GError *               error = NULL;
+    GError                *error = NULL;
     gboolean               success;
 
     g_assert(path);
@@ -915,12 +914,12 @@ static void
 test_write_duid(void)
 {
     const guint8 duid[] = {000, 001, 000, 001, 027, 'X', 0350, 'X', 0, '#', 025, 010, '~', 0254};
-    const char * expected_contents =
+    const char  *expected_contents =
         "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n";
-    GError *      error    = NULL;
+    GError       *error    = NULL;
     gs_free char *contents = NULL;
     gboolean      success;
-    const char *  path = "test-dhclient-write-duid.leases";
+    const char   *path = "test-dhclient-write-duid.leases";
 
     _save_duid(path, duid, G_N_ELEMENTS(duid));
 
@@ -941,10 +940,10 @@ test_write_existing_duid(void)
         "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n";
     const char *expected_contents =
         "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n";
-    GError *      error    = NULL;
+    GError       *error    = NULL;
     gs_free char *contents = NULL;
     gboolean      success;
-    const char *  path = "test-dhclient-write-existing-duid.leases";
+    const char   *path = "test-dhclient-write-existing-duid.leases";
 
     success = g_file_set_contents(path, original_contents, -1, &error);
     nmtst_assert_success(success, error);
@@ -968,11 +967,11 @@ static void
 test_write_existing_commented_duid(void)
 {
 #define ORIG_CONTENTS "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"
-    const char *  expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS;
-    GError *      error             = NULL;
+    const char   *expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS;
+    GError       *error             = NULL;
     gs_free char *contents          = NULL;
     gboolean      success;
-    const char *  path = "test-dhclient-write-existing-commented-duid.leases";
+    const char   *path = "test-dhclient-write-existing-commented-duid.leases";
 
     success = g_file_set_contents(path, ORIG_CONTENTS, -1, &error);
     nmtst_assert_success(success, error);
@@ -995,9 +994,9 @@ test_write_existing_multiline_duid(void)
 #define ORIG_CONTENTS              \
     "### Commented old DUID ###\n" \
     "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n"
-    const char *                expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS;
-    GError *                    error             = NULL;
-    gs_free char *              contents          = NULL;
+    const char                 *expected_contents = "default-duid \"" DUID "\";\n" ORIG_CONTENTS;
+    GError                     *error             = NULL;
+    gs_free char               *contents          = NULL;
     gboolean                    success;
     nmtst_auto_unlinkfile char *path =
         g_strdup("test-dhclient-write-existing-multiline-duid.leases");
diff --git a/src/core/dhcp/tests/test-dhcp-utils.c b/src/core/dhcp/tests/test-dhcp-utils.c
index 4d47e7e2..f021b642 100644
--- a/src/core/dhcp/tests/test-dhcp-utils.c
+++ b/src/core/dhcp/tests/test-dhcp-utils.c
@@ -20,20 +20,18 @@
 
 /*****************************************************************************/
 
-static NMIP4Config *
-_ip4_config_from_options(int ifindex, const char *iface, GHashTable *options, guint32 route_metric)
+static const NML3ConfigData *
+_ip4_config_from_options(int ifindex, const char *iface, GHashTable *options)
 {
     nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = nm_dedup_multi_index_new();
-    NMIP4Config *                                      config;
-
-    config = nm_dhcp_utils_ip4_config_from_options(multi_idx,
-                                                   ifindex,
-                                                   iface,
-                                                   options,
-                                                   RT_TABLE_MAIN,
-                                                   route_metric);
-    g_assert(config);
-    return config;
+    NML3ConfigData                                    *l3cd;
+
+    l3cd = nm_dhcp_utils_ip4_config_from_options(multi_idx, ifindex, iface, options);
+    g_assert(NM_IS_L3_CONFIG_DATA(l3cd));
+    g_assert(!nm_l3_config_data_is_sealed(l3cd));
+    if (nmtst_get_rand_bool())
+        nm_l3_config_data_seal(l3cd);
+    return l3cd;
 }
 
 typedef struct {
@@ -74,58 +72,57 @@ static const Option generic_options[] = {
 static void
 test_generic_options(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    const NMPlatformIP4Address * address;
-    const NMPlatformIP4Route *   route;
-    guint32                      tmp;
-    const char *                 expected_addr        = "192.168.1.106";
-    const char *                 expected_gw          = "192.168.1.1";
-    const char *                 expected_dns1        = "216.254.95.2";
-    const char *                 expected_dns2        = "216.231.41.2";
-    const char *                 expected_search1     = "foobar.com";
-    const char *                 expected_search2     = "blah.foobar.com";
-    const char *                 expected_route1_dest = "10.1.1.5";
-    const char *                 expected_route1_gw   = "10.1.1.1";
-    const char *                 expected_route2_dest = "100.99.88.56";
-    const char *                 expected_route2_gw   = "10.1.1.1";
-
-    options    = fill_table(generic_options, NULL);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
-
-    /* IP4 address */
-    g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1);
-    address = _nmtst_ip4_config_get_address(ip4_config, 0);
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
+    const NMPlatformIP4Address              *address;
+    const NMPlatformIP4Route                *route;
+    guint32                                  tmp;
+    const char                              *expected_addr        = "192.168.1.106";
+    const char                              *expected_gw          = "192.168.1.1";
+    const char                              *expected_dns1        = "216.254.95.2";
+    const char                              *expected_dns2        = "216.231.41.2";
+    const char                              *expected_search1     = "foobar.com";
+    const char                              *expected_search2     = "blah.foobar.com";
+    const char                              *expected_route1_dest = "10.1.1.5";
+    const char                              *expected_route1_gw   = "10.1.1.1";
+    const char                              *expected_route2_dest = "100.99.88.56";
+    const char                              *expected_route2_gw   = "10.1.1.1";
+    const char *const                       *strarr;
+    const in_addr_t                         *ia_arr;
+    guint                                    u;
+
+    options = fill_table(generic_options, NULL);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
+
+    g_assert_cmpint(nm_l3_config_data_get_num_addresses(l3cd, AF_INET), ==, 1);
+    address = nmtst_l3_config_data_get_address_at_4(l3cd, 0);
     g_assert(inet_pton(AF_INET, expected_addr, &tmp) > 0);
     g_assert(address->address == tmp);
     g_assert(address->peer_address == tmp);
     g_assert_cmpint(address->plen, ==, 24);
 
-    /* Gateway */
-    g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0);
-    g_assert(nmtst_ip4_config_get_gateway(ip4_config) == tmp);
+    nmtst_assert_ip_address(AF_INET,
+                            nmtst_l3_config_data_get_best_gateway(l3cd, AF_INET),
+                            expected_gw);
 
-    g_assert_cmpint(nm_ip4_config_get_num_wins(ip4_config), ==, 0);
+    g_assert(!nm_l3_config_data_get_wins(l3cd, &u));
+    g_assert_cmpint(u, ==, 0);
 
-    g_assert_cmpint(nm_ip4_config_get_mtu(ip4_config), ==, 987);
+    g_assert_cmpint(nm_l3_config_data_get_mtu(l3cd), ==, 987);
 
-    /* Domain searches */
-    g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 2);
-    g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 0), ==, expected_search1);
-    g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 1), ==, expected_search2);
+    strarr = nm_l3_config_data_get_searches(l3cd, AF_INET, &u);
+    g_assert_cmpint(u, ==, 2);
+    g_assert_cmpstr(strarr[0], ==, expected_search1);
+    g_assert_cmpstr(strarr[1], ==, expected_search2);
 
-    /* DNS servers */
-    g_assert_cmpint(nm_ip4_config_get_num_nameservers(ip4_config), ==, 2);
-    g_assert(inet_pton(AF_INET, expected_dns1, &tmp) > 0);
-    g_assert(nm_ip4_config_get_nameserver(ip4_config, 0) == tmp);
-    g_assert(inet_pton(AF_INET, expected_dns2, &tmp) > 0);
-    g_assert(nm_ip4_config_get_nameserver(ip4_config, 1) == tmp);
+    ia_arr = nm_l3_config_data_get_nameservers(l3cd, AF_INET, &u);
+    g_assert_cmpint(u, ==, 2);
+    nmtst_assert_ip4_address(ia_arr[0], expected_dns1);
+    nmtst_assert_ip4_address(ia_arr[1], expected_dns2);
 
-    /* Routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
 
-    /* Route #1 */
-    route = _nmtst_ip4_config_get_route(ip4_config, 0);
+    route = nmtst_l3_config_data_get_route_at_4(l3cd, 0);
     g_assert(inet_pton(AF_INET, expected_route1_dest, &tmp) > 0);
     g_assert(route->network == tmp);
     g_assert(inet_pton(AF_INET, expected_route1_gw, &tmp) > 0);
@@ -133,76 +130,70 @@ test_generic_options(void)
     g_assert_cmpint(route->plen, ==, 32);
     g_assert_cmpint(route->metric, ==, 0);
 
-    /* Route #2 */
-    route = _nmtst_ip4_config_get_route(ip4_config, 1);
+    route = nmtst_l3_config_data_get_route_at_4(l3cd, 1);
     g_assert(route->network == nmtst_inet4_from_string(expected_route2_dest));
     g_assert(route->gateway == nmtst_inet4_from_string(expected_route2_gw));
     g_assert_cmpint(route->plen, ==, 32);
     g_assert_cmpint(route->metric, ==, 0);
 
-    route = _nmtst_ip4_config_get_route(ip4_config, 2);
+    route = nmtst_l3_config_data_get_route_at_4(l3cd, 2);
     g_assert(route->network == nmtst_inet4_from_string("0.0.0.0"));
     g_assert(route->gateway == nmtst_inet4_from_string("192.168.1.1"));
     g_assert_cmpint(route->plen, ==, 0);
     g_assert_cmpint(route->metric, ==, 0);
-
-    g_hash_table_destroy(options);
 }
 
 static void
 test_wins_options(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    const NMPlatformIP4Address * address;
-    guint32                      tmp;
-    const char *                 expected_wins1 = "63.12.199.5";
-    const char *                 expected_wins2 = "150.4.88.120";
-    static const Option          data[] = {{"netbios_name_servers", "63.12.199.5 150.4.88.120"},
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
+    const NMPlatformIP4Address              *address;
+    const char                              *expected_wins1 = "63.12.199.5";
+    const char                              *expected_wins2 = "150.4.88.120";
+    static const Option data[] = {{"netbios_name_servers", "63.12.199.5 150.4.88.120"},
                                   {NULL, NULL}};
+    const in_addr_t    *ia_arr;
+    guint               u;
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
-    /* IP4 address */
-    g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1);
-    address = _nmtst_ip4_config_get_address(ip4_config, 0);
+    g_assert_cmpint(nm_l3_config_data_get_num_addresses(l3cd, AF_INET), ==, 1);
+    address = nmtst_l3_config_data_get_address_at_4(l3cd, 0);
     g_assert(address);
-    g_assert_cmpint(nm_ip4_config_get_num_wins(ip4_config), ==, 2);
-    g_assert(inet_pton(AF_INET, expected_wins1, &tmp) > 0);
-    g_assert(nm_ip4_config_get_wins(ip4_config, 0) == tmp);
-    g_assert(inet_pton(AF_INET, expected_wins2, &tmp) > 0);
-    g_assert(nm_ip4_config_get_wins(ip4_config, 1) == tmp);
 
-    g_hash_table_destroy(options);
+    ia_arr = nm_l3_config_data_get_wins(l3cd, &u);
+    g_assert_cmpint(u, ==, 2);
+    nmtst_assert_ip4_address(ia_arr[0], expected_wins1);
+    nmtst_assert_ip4_address(ia_arr[1], expected_wins2);
 }
 
 static void
 test_vendor_option_metered(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
     static const Option data[] = {{"vendor_encapsulated_options", "ANDROID_METERED"}, {NULL, NULL}};
 
-    options    = fill_table(generic_options, NULL);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
-    g_assert(nm_ip4_config_get_metered(ip4_config) == FALSE);
-    g_hash_table_destroy(options);
-    g_clear_object(&ip4_config);
-
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
-    g_assert(nm_ip4_config_get_metered(ip4_config) == TRUE);
-    g_hash_table_destroy(options);
+    options = fill_table(generic_options, NULL);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
+    g_assert(nm_l3_config_data_get_metered(l3cd) == NM_TERNARY_DEFAULT);
+    nm_clear_pointer(&options, g_hash_table_destroy);
+    nm_clear_l3cd(&l3cd);
+
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
+    g_assert(nm_l3_config_data_get_metered(l3cd) == TRUE);
 }
 
 static void
 test_parse_search_list(void)
 {
     guint8 *data;
-    char ** domains;
+    char  **domains;
 
     data    = (guint8[]){0x05, 'l', 'o', 'c', 'a', 'l', 0x00};
     domains = nm_dhcp_lease_data_parse_search_list(data, 7);
@@ -252,18 +243,18 @@ test_parse_search_list(void)
 }
 
 static void
-ip4_test_route(NMIP4Config *ip4_config,
-               guint        route_num,
-               const char * expected_dest,
-               const char * expected_gw,
-               guint        expected_prefix)
+ip4_test_route(const NML3ConfigData *l3cd,
+               guint                 route_num,
+               const char           *expected_dest,
+               const char           *expected_gw,
+               guint                 expected_prefix)
 {
     const NMPlatformIP4Route *route;
     guint32                   tmp;
 
     g_assert(expected_prefix <= 32);
 
-    route = _nmtst_ip4_config_get_route(ip4_config, route_num);
+    route = nmtst_l3_config_data_get_route_at_4(l3cd, route_num);
     g_assert(inet_pton(AF_INET, expected_dest, &tmp) > 0);
     g_assert(route->network == tmp);
     g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0);
@@ -272,110 +263,105 @@ ip4_test_route(NMIP4Config *ip4_config,
     g_assert_cmpint(route->metric, ==, 0);
 }
 
-static void
-ip4_test_gateway(NMIP4Config *ip4_config, const char *expected_gw)
-{
-    guint32 tmp;
-
-    g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1);
-    g_assert(inet_pton(AF_INET, expected_gw, &tmp) > 0);
-    g_assert(nmtst_ip4_config_get_gateway(ip4_config) == tmp);
-}
+#define ip4_test_gateway(l3cd, expected_gw)                                            \
+    G_STMT_START                                                                       \
+    {                                                                                  \
+        const NML3ConfigData *_l3cd = (l3cd);                                          \
+                                                                                       \
+        g_assert_cmpint(nm_l3_config_data_get_num_addresses(_l3cd, AF_INET), ==, 1);   \
+        nmtst_assert_ip_address(AF_INET,                                               \
+                                nmtst_l3_config_data_get_best_gateway(_l3cd, AF_INET), \
+                                expected_gw);                                          \
+    }                                                                                  \
+    G_STMT_END
 
 static void
 test_classless_static_routes_1(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    const char *                 expected_route2_dest = "10.0.0.0";
-    const char *                 expected_route2_gw   = "10.17.66.41";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "192.168.10.0";
+    const char                              *expected_route1_gw   = "192.168.1.1";
+    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 */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 8 10 10 17 66 41"},
         {NULL, NULL}};
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 8);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 8);
+    ip4_test_route(l3cd, 2, "0.0.0.0", "192.168.1.1", 0);
 }
 
 static void
 test_classless_static_routes_2(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    const char *                 expected_route2_dest = "10.0.0.0";
-    const char *                 expected_route2_gw   = "10.17.66.41";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "192.168.10.0";
+    const char                              *expected_route1_gw   = "192.168.1.1";
+    const char                              *expected_route2_dest = "10.0.0.0";
+    const char                              *expected_route2_gw   = "10.17.66.41";
+    static const Option                      data[]               = {
         /* 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}};
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 8);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", expected_route1_gw, 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 8);
+    ip4_test_route(l3cd, 2, "0.0.0.0", expected_route1_gw, 0);
 }
 
 static void
 test_fedora_dhclient_classless_static_routes(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "129.210.177.128";
-    const char *                 expected_route1_gw   = "192.168.0.113";
-    const char *                 expected_route2_dest = "2.0.0.0";
-    const char *                 expected_route2_gw   = "10.34.255.6";
-    const char *                 expected_gateway     = "192.168.0.113";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "129.210.177.128";
+    const char                              *expected_route1_gw   = "192.168.0.113";
+    const char                              *expected_route2_dest = "2.0.0.0";
+    const char                              *expected_route2_gw   = "10.34.255.6";
+    const char                              *expected_gateway     = "192.168.0.113";
+    static const Option                      data[]               = {
         /* 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"},
         {NULL, NULL}};
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 25);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 7);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", expected_route1_gw, 0);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 25);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 7);
+    ip4_test_route(l3cd, 2, "0.0.0.0", expected_route1_gw, 0);
 
-    /* Gateway */
-    ip4_test_gateway(ip4_config, expected_gateway);
-
-    g_hash_table_destroy(options);
+    ip4_test_gateway(l3cd, expected_gateway);
 }
 
 static void
 test_dhclient_invalid_classless_routes_1(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "192.168.10.0";
+    const char                              *expected_route1_gw   = "192.168.1.1";
+    static const Option                      data[]               = {
         /* dhclient format */
         {"rfc3442_classless_static_routes", "24 192 168 10 192 168 1 1 45 10 17 66 41"},
         {NULL, NULL}};
@@ -384,27 +370,25 @@ test_dhclient_invalid_classless_routes_1(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 2);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, "0.0.0.0", expected_route1_gw, 0);
 }
 
 static void
 test_dhcpcd_invalid_classless_routes_1(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "10.1.1.5";
-    const char *                 expected_route1_gw   = "10.1.1.1";
-    const char *                 expected_route2_dest = "100.99.88.56";
-    const char *                 expected_route2_gw   = "10.1.1.1";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "10.1.1.5";
+    const char                              *expected_route1_gw   = "10.1.1.1";
+    const char                              *expected_route2_dest = "100.99.88.56";
+    const char                              *expected_route2_gw   = "10.1.1.1";
+    static const Option                      data[]               = {
         /* dhcpcd format */
         {"classless_static_routes", "192.168.10.0/24 192.168.1.1 10.0.adfadf/44 10.17.66.41"},
         {NULL, NULL}};
@@ -413,30 +397,28 @@ test_dhcpcd_invalid_classless_routes_1(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* Test falling back to old-style static routes if the classless static
      * routes are invalid.
      */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 32);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 32);
+    ip4_test_route(l3cd, 2, "0.0.0.0", "192.168.1.1", 0);
 }
 
 static void
 test_dhclient_invalid_classless_routes_2(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "10.1.1.5";
-    const char *                 expected_route1_gw   = "10.1.1.1";
-    const char *                 expected_route2_dest = "100.99.88.56";
-    const char *                 expected_route2_gw   = "10.1.1.1";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "10.1.1.5";
+    const char                              *expected_route1_gw   = "10.1.1.1";
+    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}};
 
@@ -444,30 +426,28 @@ test_dhclient_invalid_classless_routes_2(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* Test falling back to old-style static routes if the classless static
      * routes are invalid.
      */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 32);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 32);
+    ip4_test_route(l3cd, 2, "0.0.0.0", "192.168.1.1", 0);
 }
 
 static void
 test_dhcpcd_invalid_classless_routes_2(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "10.1.1.5";
-    const char *                 expected_route1_gw   = "10.1.1.1";
-    const char *                 expected_route2_dest = "100.99.88.56";
-    const char *                 expected_route2_gw   = "10.1.1.1";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "10.1.1.5";
+    const char                              *expected_route1_gw   = "10.1.1.1";
+    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}};
 
@@ -475,7 +455,7 @@ test_dhcpcd_invalid_classless_routes_2(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* Test falling back to old-style static routes if the classless static
@@ -483,22 +463,20 @@ test_dhcpcd_invalid_classless_routes_2(void)
      */
 
     /* Routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 3);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 32);
-    ip4_test_route(ip4_config, 1, expected_route2_dest, expected_route2_gw, 32);
-    ip4_test_route(ip4_config, 2, "0.0.0.0", "192.168.1.1", 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 3);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 32);
+    ip4_test_route(l3cd, 1, expected_route2_dest, expected_route2_gw, 32);
+    ip4_test_route(l3cd, 2, "0.0.0.0", "192.168.1.1", 0);
 }
 
 static void
 test_dhclient_invalid_classless_routes_3(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    static const Option          data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    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}};
 
@@ -506,25 +484,23 @@ test_dhclient_invalid_classless_routes_3(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*ignoring invalid classless static routes*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 2);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, "0.0.0.0", expected_route1_gw, 0);
 }
 
 static void
 test_dhcpcd_invalid_classless_routes_3(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    static Option                data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    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}};
 
@@ -532,133 +508,124 @@ test_dhcpcd_invalid_classless_routes_3(void)
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*DHCP provided invalid classless static route*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, "0.0.0.0", expected_route1_gw, 0);
-
-    g_hash_table_destroy(options);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 2);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, "0.0.0.0", expected_route1_gw, 0);
 }
 
 static void
 test_dhclient_gw_in_classless_routes(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    const char *                 expected_gateway     = "192.2.3.4";
-    static Option                data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "192.168.10.0";
+    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}};
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, "0.0.0.0", "192.2.3.4", 0);
-
-    /* Gateway */
-    ip4_test_gateway(ip4_config, expected_gateway);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 2);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, "0.0.0.0", "192.2.3.4", 0);
 
-    g_hash_table_destroy(options);
+    ip4_test_gateway(l3cd, expected_gateway);
 }
 
 static void
 test_dhcpcd_gw_in_classless_routes(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config           = NULL;
-    const char *                 expected_route1_dest = "192.168.10.0";
-    const char *                 expected_route1_gw   = "192.168.1.1";
-    const char *                 expected_gateway     = "192.2.3.4";
-    static Option                data[]               = {
+    gs_unref_hashtable GHashTable           *options              = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd                 = NULL;
+    const char                              *expected_route1_dest = "192.168.10.0";
+    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}};
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
     /* IP4 routes */
-    g_assert_cmpint(nm_ip4_config_get_num_routes(ip4_config), ==, 2);
-    ip4_test_route(ip4_config, 0, expected_route1_dest, expected_route1_gw, 24);
-    ip4_test_route(ip4_config, 1, "0.0.0.0", "192.2.3.4", 0);
-
-    /* Gateway */
-    ip4_test_gateway(ip4_config, expected_gateway);
+    g_assert_cmpint(nm_l3_config_data_get_num_routes(l3cd, AF_INET), ==, 2);
+    ip4_test_route(l3cd, 0, expected_route1_dest, expected_route1_gw, 24);
+    ip4_test_route(l3cd, 1, "0.0.0.0", "192.2.3.4", 0);
 
-    g_hash_table_destroy(options);
+    ip4_test_gateway(l3cd, expected_gateway);
 }
 
 static void
 test_escaped_domain_searches(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config       = NULL;
-    const char *                 expected_search0 = "host1";
-    const char *                 expected_search1 = "host2";
-    const char *                 expected_search2 = "host3";
+    gs_unref_hashtable GHashTable           *options          = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd             = NULL;
+    const char                              *expected_search0 = "host1";
+    const char                              *expected_search1 = "host2";
+    const char                              *expected_search2 = "host3";
     static const Option data[] = {{"domain_search", "host1\\032host2\\032host3"}, {NULL, NULL}};
+    const char *const  *strarr;
+    guint               u;
 
-    options    = fill_table(generic_options, NULL);
-    options    = fill_table(data, options);
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
-
-    /* domain searches */
-    g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 3);
-    g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 0), ==, expected_search0);
-    g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 1), ==, expected_search1);
-    g_assert_cmpstr(nm_ip4_config_get_search(ip4_config, 2), ==, expected_search2);
+    options = fill_table(generic_options, NULL);
+    options = fill_table(data, options);
+    l3cd    = _ip4_config_from_options(1, "eth0", options);
 
-    g_hash_table_destroy(options);
+    strarr = nm_l3_config_data_get_searches(l3cd, AF_INET, &u);
+    g_assert_cmpint(u, ==, 3);
+    g_assert_cmpstr(strarr[0], ==, expected_search0);
+    g_assert_cmpstr(strarr[1], ==, expected_search1);
+    g_assert_cmpstr(strarr[2], ==, expected_search2);
 }
 
 static void
 test_invalid_escaped_domain_searches(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
     static const Option data[] = {{"domain_search", "host1\\aahost2\\032host3"}, {NULL, NULL}};
+    const char *const  *strarr;
+    guint               u;
 
     options = fill_table(generic_options, NULL);
     options = fill_table(data, options);
 
     NMTST_EXPECT_NM_WARN("*invalid domain search*");
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
     g_test_assert_expected_messages();
 
-    /* domain searches */
-    g_assert_cmpint(nm_ip4_config_get_num_searches(ip4_config), ==, 0);
-
-    g_hash_table_destroy(options);
+    strarr = nm_l3_config_data_get_searches(l3cd, AF_INET, &u);
+    g_assert_cmpint(u, ==, 0);
+    g_assert(!strarr);
 }
 
 static void
 test_ip4_missing_prefix(const char *ip, guint32 expected_prefix)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    const NMPlatformIP4Address * address;
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
+    const NMPlatformIP4Address              *address;
 
     options = fill_table(generic_options, NULL);
     g_hash_table_insert(options, "ip_address", (gpointer) ip);
     g_hash_table_remove(options, "subnet_mask");
 
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
 
-    g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1);
-    address = _nmtst_ip4_config_get_address(ip4_config, 0);
+    g_assert_cmpint(nm_l3_config_data_get_num_addresses(l3cd, AF_INET), ==, 1);
+    address = nmtst_l3_config_data_get_address_at_4(l3cd, 0);
     g_assert(address);
     g_assert_cmpint(address->plen, ==, expected_prefix);
-
-    g_hash_table_destroy(options);
 }
 
 static void
@@ -682,9 +649,9 @@ test_ip4_missing_prefix_8(void)
 static void
 test_ip4_prefix_classless(void)
 {
-    GHashTable *    options;
-    gs_unref_object NMIP4Config *ip4_config = NULL;
-    const NMPlatformIP4Address * address;
+    gs_unref_hashtable GHashTable           *options = NULL;
+    nm_auto_unref_l3cd const NML3ConfigData *l3cd    = NULL;
+    const NMPlatformIP4Address              *address;
 
     /* Ensure that the missing-subnet-mask handler doesn't mangle classless
      * subnet masks at all.  The handler should trigger only if the server
@@ -695,21 +662,19 @@ test_ip4_prefix_classless(void)
     g_hash_table_insert(options, "ip_address", "172.16.54.22");
     g_hash_table_insert(options, "subnet_mask", "255.255.252.0");
 
-    ip4_config = _ip4_config_from_options(1, "eth0", options, 0);
+    l3cd = _ip4_config_from_options(1, "eth0", options);
 
-    g_assert_cmpint(nm_ip4_config_get_num_addresses(ip4_config), ==, 1);
-    address = _nmtst_ip4_config_get_address(ip4_config, 0);
+    g_assert_cmpint(nm_l3_config_data_get_num_addresses(l3cd, AF_INET), ==, 1);
+    address = nmtst_l3_config_data_get_address_at_4(l3cd, 0);
     g_assert(address);
     g_assert_cmpint(address->plen, ==, 22);
-
-    g_hash_table_destroy(options);
 }
 
 #define COMPARE_ID(src, is_str, expected, expected_len)           \
     G_STMT_START                                                  \
     {                                                             \
         gs_unref_bytes GBytes *b = NULL;                          \
-        const char *           p;                                 \
+        const char            *p;                                 \
         gsize                  l;                                 \
                                                                   \
         b = nm_dhcp_utils_client_id_string_to_bytes(src);         \
@@ -729,12 +694,12 @@ test_ip4_prefix_classless(void)
 static void
 test_client_id_from_string(void)
 {
-    const char * nothex       = "asdfasdfasdfasdfasdfasdfasdf";
-    const char * allhex       = "00:11:22:33:4:55:66:77:88";
+    const char  *nothex       = "asdfasdfasdfasdfasdfasdfasdf";
+    const char  *allhex       = "00:11:22:33:4:55:66:77:88";
     const guint8 allhex_bin[] = {0x00, 0x11, 0x22, 0x33, 0x04, 0x55, 0x66, 0x77, 0x88};
-    const char * somehex      = "00:11:22:33:44:55:asdfasdfasdf:99:10";
-    const char * nocolons     = "0011223344559910";
-    const char * endcolon     = "00:11:22:33:44:55:";
+    const char  *somehex      = "00:11:22:33:44:55:asdfasdfasdf:99:10";
+    const char  *nocolons     = "0011223344559910";
+    const char  *endcolon     = "00:11:22:33:44:55:";
 
     COMPARE_ID(nothex, TRUE, nothex, strlen(nothex));
     COMPARE_ID(allhex, FALSE, allhex_bin, sizeof(allhex_bin));