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.c117
-rw-r--r--src/core/dhcp/nm-dhcp-client.h17
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.c91
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient-utils.h5
-rw-r--r--src/core/dhcp/nm-dhcp-dhclient.c39
-rw-r--r--src/core/dhcp/nm-dhcp-nettools.c17
-rw-r--r--src/core/dhcp/nm-dhcp-options.c8
-rw-r--r--src/core/dhcp/nm-dhcp-options.h9
-rw-r--r--src/core/dhcp/nm-dhcp-systemd.c6
-rw-r--r--src/core/dhcp/tests/test-dhcp-dhclient.c248
10 files changed, 354 insertions, 203 deletions
diff --git a/src/core/dhcp/nm-dhcp-client.c b/src/core/dhcp/nm-dhcp-client.c
index 1329b953..805b42d2 100644
--- a/src/core/dhcp/nm-dhcp-client.c
+++ b/src/core/dhcp/nm-dhcp-client.c
@@ -237,30 +237,65 @@ nm_dhcp_client_create_l3cd(NMDhcpClient *self)
                                  NM_IP_CONFIG_SOURCE_DHCP);
 }
 
+GHashTable *
+nm_dhcp_client_create_options_dict(NMDhcpClient *self, gboolean static_keys)
+{
+    NMDhcpClientPrivate *priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    GHashTable          *options;
+    GBytes              *effective_client_id;
+
+    options = nm_dhcp_option_create_options_dict(static_keys);
+
+    effective_client_id = nm_dhcp_client_get_effective_client_id(self);
+    if (effective_client_id) {
+        guint         option = NM_IS_IPv4(priv->config.addr_family) ? NM_DHCP_OPTION_DHCP4_CLIENT_ID
+                                                                    : NM_DHCP_OPTION_DHCP6_CLIENT_ID;
+        gs_free char *str    = nm_dhcp_utils_duid_to_string(effective_client_id);
+
+        /* Note that for the nm-dhcp-helper based plugins (dhclient), the plugin
+         * may send the used client-id/DUID via the environment variables and
+         * overwrite them yet again. */
+
+        if (static_keys) {
+            nm_dhcp_option_add_option(options, priv->config.addr_family, option, str);
+        } else {
+            g_hash_table_insert(
+                options,
+                g_strdup(nm_dhcp_option_request_string(priv->config.addr_family, option)),
+                g_steal_pointer(&str));
+        }
+    }
+
+    return options;
+}
+
 /*****************************************************************************/
 
-void
+gboolean
 nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id)
 {
-    NMDhcpClientPrivate *priv    = NM_DHCP_CLIENT_GET_PRIVATE(self);
-    gs_free char        *tmp_str = NULL;
+    NMDhcpClientPrivate   *priv              = NM_DHCP_CLIENT_GET_PRIVATE(self);
+    gs_free char          *tmp_str           = NULL;
+    gs_unref_bytes GBytes *client_id_to_free = NULL;
 
-    g_return_if_fail(NM_IS_DHCP_CLIENT(self));
-    g_return_if_fail(!client_id || g_bytes_get_size(client_id) >= 2);
+    g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
+    g_return_val_if_fail(!client_id || g_bytes_get_size(client_id) >= 2, FALSE);
 
     priv = NM_DHCP_CLIENT_GET_PRIVATE(self);
 
     if (nm_g_bytes_equal0(priv->effective_client_id, client_id))
-        return;
+        return FALSE;
 
-    g_bytes_unref(priv->effective_client_id);
+    client_id_to_free         = g_steal_pointer(&priv->effective_client_id);
     priv->effective_client_id = nm_g_bytes_ref(client_id);
 
-    _LOGT("%s: set %s",
+    _LOGT("%s: set effective %s",
           priv->config.addr_family == AF_INET6 ? "duid" : "client-id",
           priv->effective_client_id
               ? (tmp_str = nm_dhcp_utils_duid_to_string(priv->effective_client_id))
               : "default");
+
+    return TRUE;
 }
 
 /*****************************************************************************/
@@ -967,12 +1002,6 @@ _dhcp_client_decline(NMDhcpClient         *self,
     return klass->decline(self, l3cd, error_message, error);
 }
 
-static GBytes *
-get_duid(NMDhcpClient *self)
-{
-    return NULL;
-}
-
 static gboolean
 ipv6_lladdr_timeout(gpointer user_data)
 {
@@ -1302,8 +1331,7 @@ gboolean
 nm_dhcp_client_start(NMDhcpClient *self, GError **error)
 {
     NMDhcpClientPrivate        *priv;
-    gs_unref_bytes GBytes      *own_client_id = NULL;
-    const NMPlatformIP6Address *addr          = NULL;
+    const NMPlatformIP6Address *addr = NULL;
     int                         IS_IPv4;
 
     g_return_val_if_fail(NM_IS_DHCP_CLIENT(self), FALSE);
@@ -1317,11 +1345,6 @@ nm_dhcp_client_start(NMDhcpClient *self, GError **error)
     IS_IPv4 = NM_IS_IPv4(priv->config.addr_family);
 
     if (!IS_IPv4) {
-        if (!priv->config.v6.enforce_duid)
-            own_client_id = NM_DHCP_CLIENT_GET_CLASS(self)->get_duid(self);
-
-        nm_dhcp_client_set_effective_client_id(self, own_client_id ?: priv->config.client_id);
-
         addr = ipv6_lladdr_find(self);
         if (!addr) {
             _LOGD("waiting for IPv6LL address");
@@ -1449,7 +1472,7 @@ nm_dhcp_client_stop(NMDhcpClient *self, gboolean release)
 /*****************************************************************************/
 
 static char *
-bytearray_variant_to_string(NMDhcpClient *self, GVariant *value, const char *key)
+bytearray_variant_to_string(GVariant *value)
 {
     const guint8 *array;
     char         *str;
@@ -1499,8 +1522,9 @@ label_is_unknown_xyz(const char *label)
 static void
 maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant *value)
 {
-    char *str_value;
-    int   priv_opt_num;
+    const int IS_IPv4 = NM_IS_IPv4(NM_DHCP_CLIENT_GET_PRIVATE(self)->config.addr_family);
+    char     *str_value;
+    int       priv_opt_num;
 
     if (!g_variant_is_of_type(value, G_VARIANT_TYPE_BYTESTRING))
         return;
@@ -1517,27 +1541,49 @@ maybe_add_option(NMDhcpClient *self, GHashTable *hash, const char *key, GVariant
     if (NM_STR_HAS_PREFIX(key, "private_") || !key[0])
         return;
 
-    str_value = bytearray_variant_to_string(self, value, key);
+    str_value = bytearray_variant_to_string(value);
     if (!str_value)
         return;
 
+    if ((IS_IPv4 && nm_streq(key, "dhcp_client_identifier"))
+        || (!IS_IPv4 && nm_streq(key, "dhcp6_client_id"))) {
+        gs_free char          *str   = g_steal_pointer(&str_value);
+        gs_unref_bytes GBytes *bytes = NULL;
+
+        /* Validate and normalize the client-id/DUID. */
+
+        bytes = nm_utils_hexstr2bin(str);
+        if (!bytes || g_bytes_get_size(bytes) < 2) {
+            /* Seems invalid. Ignore */
+            return;
+        }
+
+        if (!nm_dhcp_client_set_effective_client_id(self, bytes)) {
+            /* the client-id is identical and we already set it. Nothing to do. */
+            return;
+        }
+
+        /* The effective-client-id was (re)set. Update "hash" with the new value... */
+        str_value = nm_dhcp_utils_duid_to_string(bytes);
+    }
+
     g_hash_table_insert(hash, g_strdup(key), str_value);
 
     /* dhclient has no special labels for private dhcp options: it uses "unknown_xyz"
-         * labels for that. We need to identify those to alias them to our "private_xyz"
-         * format unused in the internal dchp plugins.
-         */
+     * labels for that. We need to identify those to alias them to our "private_xyz"
+     * format unused in the internal dchp plugins.
+     */
     if ((priv_opt_num = label_is_unknown_xyz(key)) > 0) {
         gs_free guint8 *check_val = NULL;
         char           *hex_str   = NULL;
         gsize           len;
 
         /* dhclient passes values from dhcp private options in its own "string" format:
-             * if the raw values are printable as ascii strings, it will pass the string
-             * representation; if the values are not printable as an ascii string, it will
-             * pass a string displaying the hex values (hex string). Try to enforce passing
-             * always an hex string, converting string representation if needed.
-             */
+         * if the raw values are printable as ascii strings, it will pass the string
+         * representation; if the values are not printable as an ascii string, it will
+         * pass a string displaying the hex values (hex string). Try to enforce passing
+         * always an hex string, converting string representation if needed.
+         */
         check_val = nm_utils_hexstr2bin_alloc(str_value, FALSE, TRUE, ":", 0, &len);
         hex_str   = nm_utils_bin2hexstr_full(check_val ?: (guint8 *) str_value,
                                            check_val ? len : strlen(str_value),
@@ -1623,7 +1669,7 @@ nm_dhcp_client_handle_event(gpointer               unused,
         GVariant                      *value;
 
         /* Copy options */
-        str_options = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, g_free);
+        str_options = nm_dhcp_client_create_options_dict(self, FALSE);
         g_variant_iter_init(&iter, options);
         while (g_variant_iter_next(&iter, "{&sv}", &name, &value)) {
             maybe_add_option(self, str_options, name, value);
@@ -1914,8 +1960,7 @@ nm_dhcp_client_class_init(NMDhcpClientClass *client_class)
     client_class->accept       = _accept;
     client_class->decline      = decline;
 
-    client_class->stop     = stop;
-    client_class->get_duid = get_duid;
+    client_class->stop = stop;
 
     obj_properties[PROP_CONFIG] =
         g_param_spec_pointer(NM_DHCP_CLIENT_CONFIG,
diff --git a/src/core/dhcp/nm-dhcp-client.h b/src/core/dhcp/nm-dhcp-client.h
index 51c6bc04..6f403b61 100644
--- a/src/core/dhcp/nm-dhcp-client.h
+++ b/src/core/dhcp/nm-dhcp-client.h
@@ -214,17 +214,6 @@ typedef struct {
     gboolean (*ip6_start)(NMDhcpClient *self, const struct in6_addr *ll_addr, GError **error);
 
     void (*stop)(NMDhcpClient *self, gboolean release);
-
-    /**
-     * get_duid:
-     * @self: the #NMDhcpClient
-     *
-     * Attempts to find an existing DHCPv6 DUID for this client in the DHCP
-     * client's persistent configuration.  Returned DUID should be the binary
-     * representation of the DUID.  If no DUID is found, %NULL should be
-     * returned.
-     */
-    GBytes *(*get_duid)(NMDhcpClient *self);
 } NMDhcpClientClass;
 
 GType nm_dhcp_client_get_type(void);
@@ -285,11 +274,13 @@ 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);
+gboolean nm_dhcp_client_set_effective_client_id(NMDhcpClient *self, GBytes *client_id);
+GBytes  *nm_dhcp_client_get_effective_client_id(NMDhcpClient *self);
 
 NML3ConfigData *nm_dhcp_client_create_l3cd(NMDhcpClient *self);
 
+GHashTable *nm_dhcp_client_create_options_dict(NMDhcpClient *self, gboolean static_keys);
+
 /*****************************************************************************
  * Client data
  *****************************************************************************/
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.c b/src/core/dhcp/nm-dhcp-dhclient-utils.c
index 74e6b905..ea8943fa 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.c
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.c
@@ -399,6 +399,7 @@ nm_dhcp_dhclient_create_config(const char         *interface,
                 if (out_new_client_id)
                     nm_clear_pointer(out_new_client_id, g_bytes_unref);
                 NM_SET_OUT(out_new_client_id, read_client_id(p));
+                /* fall-through. We keep the line... */
             }
 
             /* Override config file hostname and use one from the connection */
@@ -523,6 +524,20 @@ nm_dhcp_dhclient_create_config(const char         *interface,
     return g_string_free(g_steal_pointer(&new_contents), FALSE);
 }
 
+/* In the lease file, dhclient will write "option dhcp6.client-id $HEXSTR". This
+ * function does the same. */
+static char *
+nm_dhcp_dhclient_escape_duid_as_hex(GBytes *duid)
+{
+    const guint8 *s;
+    gsize         len;
+
+    nm_assert(duid);
+
+    s = g_bytes_get_data(duid, &len);
+    return nm_utils_bin2hexstr_fuller(s, len, ':', FALSE, FALSE, NULL);
+}
+
 /* Roughly follow what dhclient's quotify_buf() and pretty_escape() functions do */
 char *
 nm_dhcp_dhclient_escape_duid(GBytes *duid)
@@ -605,7 +620,7 @@ error:
     return NULL;
 }
 
-#define DUID_PREFIX "default-duid \""
+#define DEFAULT_DUID_PREFIX "default-duid \""
 
 /* Beware: @error may be unset even if the function returns %NULL. */
 GBytes *
@@ -626,10 +641,10 @@ nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
         const char *p = nm_str_skip_leading_spaces(contents_v[i]);
         GBytes     *duid;
 
-        if (!NM_STR_HAS_PREFIX(p, DUID_PREFIX))
+        if (!NM_STR_HAS_PREFIX(p, DEFAULT_DUID_PREFIX))
             continue;
 
-        p += NM_STRLEN(DUID_PREFIX);
+        p += NM_STRLEN(DEFAULT_DUID_PREFIX);
 
         g_strchomp((char *) p);
 
@@ -647,15 +662,21 @@ nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error)
 }
 
 gboolean
-nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error)
+nm_dhcp_dhclient_save_duid(const char *leasefile,
+                           GBytes     *duid,
+                           gboolean    enforce_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;
-    gsize                         len = 0;
+    gs_free char                 *conflicting_duid_line = NULL;
+    gs_free char                 *contents              = NULL;
+    gsize                         contents_len          = 0;
 
     g_return_val_if_fail(leasefile != NULL, FALSE);
+
     if (!duid) {
         nm_utils_error_set_literal(error, NM_UTILS_ERROR_UNKNOWN, "missing duid");
         g_return_val_if_reached(FALSE);
@@ -665,48 +686,68 @@ nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error)
     nm_assert(escaped_duid);
 
     if (g_file_test(leasefile, G_FILE_TEST_EXISTS)) {
-        gs_free char *contents = NULL;
-
-        if (!g_file_get_contents(leasefile, &contents, &len, error)) {
+        if (!g_file_get_contents(leasefile, &contents, &contents_len, error)) {
             g_prefix_error(error, "failed to read lease file %s: ", leasefile);
             return FALSE;
         }
 
-        lines = nm_strsplit_set_with_empty(contents, "\n\r");
+        lines = nm_strsplit_set_with_empty(contents, "\n");
     }
 
-    s = g_string_sized_new(len + 50);
-    g_string_append_printf(s, DUID_PREFIX "%s\";\n", escaped_duid);
+    s = g_string_sized_new(contents_len + 50);
+    g_string_append_printf(s, DEFAULT_DUID_PREFIX "%s\";\n", escaped_duid);
 
     /* Preserve existing leasefile contents */
     if (lines) {
         for (iter = lines; *iter; iter++) {
             const char *str = *iter;
             const char *l;
+            gboolean    ends_with_r;
+            gsize       l_len;
+            gsize       prefix_len;
 
-            /* If we find an uncommented DUID in the file, check if
-             * equal to the one we are going to write: if so, no need
-             * to update the lease file, otherwise skip the old DUID.
-             */
-            l = nm_str_skip_leading_spaces(str);
-            if (g_str_has_prefix(l, DUID_PREFIX)) {
-                gs_strfreev char **split = NULL;
+            l          = nm_str_skip_leading_spaces(str);
+            l_len      = strlen(l);
+            prefix_len = l - str;
 
-                split = g_strsplit(l, "\"", -1);
-                if (split[0] && nm_streq0(split[1], escaped_duid))
-                    return TRUE;
+            ends_with_r = l_len > 0 && l[l_len - 1u] == '\r';
+            if (ends_with_r) {
+                ((char *) l)[--l_len] = '\0';
+            }
 
+            if (NM_STR_HAS_PREFIX(l, DEFAULT_DUID_PREFIX)) {
+                /* We always add our line on top. This line can be skipped. */
                 continue;
             }
 
-            if (str)
-                g_string_append(s, str);
-            /* avoid to add an extra '\n' at the end of file */
-            if ((iter[1]) != NULL)
+            if (enforce_duid & NM_STR_HAS_PREFIX(l, "option dhcp6.client-id ")) {
+                /* we want to use our duid. Skip the per-lease client-id. */
+                if (!conflicting_duid_line) {
+                    gs_free char *duid_hex = nm_dhcp_dhclient_escape_duid_as_hex(duid);
+
+                    conflicting_duid_line = g_strdup_printf("option dhcp6.client-id %s;", duid_hex);
+                }
+                /* We adjust the duid line and set what we want. */
+                l = conflicting_duid_line;
+            }
+
+            g_string_append_len(s, str, prefix_len);
+            g_string_append(s, l);
+            if (ends_with_r) {
+                g_string_append_c(s, '\r');
                 g_string_append_c(s, '\n');
+            } else if ((iter[1]) != NULL) {
+                /* avoid to add an extra '\n' at the end of file */
+                g_string_append_c(s, '\n');
+            }
         }
     }
 
+    if (contents && strlen(contents) == contents_len && nm_streq(contents, s->str)) {
+        /* The file is already as we want it. We are done. */
+        return TRUE;
+    }
+
     if (!g_file_set_contents(leasefile, s->str, -1, error)) {
         g_prefix_error(error, "failed to set DUID in lease file %s: ", leasefile);
         return FALSE;
diff --git a/src/core/dhcp/nm-dhcp-dhclient-utils.h b/src/core/dhcp/nm-dhcp-dhclient-utils.h
index 0ce7dd90..6187cce0 100644
--- a/src/core/dhcp/nm-dhcp-dhclient-utils.h
+++ b/src/core/dhcp/nm-dhcp-dhclient-utils.h
@@ -29,6 +29,9 @@ GBytes *nm_dhcp_dhclient_unescape_duid(const char *duid);
 
 GBytes *nm_dhcp_dhclient_read_duid(const char *leasefile, GError **error);
 
-gboolean nm_dhcp_dhclient_save_duid(const char *leasefile, GBytes *duid, GError **error);
+gboolean nm_dhcp_dhclient_save_duid(const char *leasefile,
+                                    GBytes     *duid,
+                                    gboolean    enforce_duid,
+                                    GError    **error);
 
 #endif /* __NETWORKMANAGER_DHCP_DHCLIENT_UTILS_H__ */
diff --git a/src/core/dhcp/nm-dhcp-dhclient.c b/src/core/dhcp/nm-dhcp-dhclient.c
index d0cd5ebd..e4f40d7c 100644
--- a/src/core/dhcp/nm-dhcp-dhclient.c
+++ b/src/core/dhcp/nm-dhcp-dhclient.c
@@ -82,6 +82,10 @@ G_DEFINE_TYPE(NMDhcpDhclient, nm_dhcp_dhclient, NM_TYPE_DHCP_CLIENT)
 
 /*****************************************************************************/
 
+static GBytes *read_duid_from_lease(NMDhcpDhclient *self);
+
+/*****************************************************************************/
+
 static const char *
 nm_dhcp_dhclient_get_path(void)
 {
@@ -332,6 +336,7 @@ static gboolean
 dhclient_start(NMDhcpClient *client,
                gboolean      set_mode,
                gboolean      release,
+               gboolean      set_duid,
                pid_t        *out_pid,
                GError      **error)
 {
@@ -410,8 +415,11 @@ 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, client_config->client_id, &local)) {
+    if (set_duid && addr_family == AF_INET6) {
+        if (!nm_dhcp_dhclient_save_duid(priv->lease_file,
+                                        nm_dhcp_client_get_effective_client_id(client),
+                                        client_config->v6.enforce_duid,
+                                        &local)) {
             nm_utils_error_set(error,
                                NM_UTILS_ERROR_UNKNOWN,
                                "failed to save DUID to '%s': %s",
@@ -547,11 +555,11 @@ ip4_start(NMDhcpClient *client, GError **error)
         return FALSE;
     }
 
-    if (new_client_id) {
-        nm_assert(!client_config->client_id);
-        nm_dhcp_client_set_effective_client_id(client, new_client_id);
-    }
-    return dhclient_start(client, FALSE, FALSE, NULL, error);
+    /* Note that the effective-client-id for IPv4 here might be unknown/NULL. */
+    nm_assert(!new_client_id || !client_config->client_id);
+    nm_dhcp_client_set_effective_client_id(client, client_config->client_id ?: new_client_id);
+
+    return dhclient_start(client, FALSE, FALSE, FALSE, NULL, error);
 }
 
 static gboolean
@@ -560,6 +568,7 @@ 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);
     const NMDhcpClientConfig *config;
+    gs_unref_bytes GBytes    *effective_client_id = NULL;
 
     config = nm_dhcp_client_get_config(client);
 
@@ -586,7 +595,12 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
         return FALSE;
     }
 
-    return dhclient_start(client, TRUE, FALSE, NULL, error);
+    nm_assert(config->client_id);
+    if (!config->v6.enforce_duid)
+        effective_client_id = read_duid_from_lease(self);
+    nm_dhcp_client_set_effective_client_id(client, effective_client_id ?: config->client_id);
+
+    return dhclient_start(client, TRUE, FALSE, TRUE, NULL, error);
 }
 
 static void
@@ -620,7 +634,7 @@ stop(NMDhcpClient *client, gboolean release)
     if (release) {
         pid_t rpid = -1;
 
-        if (dhclient_start(client, FALSE, TRUE, &rpid, NULL)) {
+        if (dhclient_start(client, FALSE, TRUE, FALSE, &rpid, NULL)) {
             /* Wait a few seconds for the release to happen */
             nm_dhcp_client_stop_pid(rpid, nm_dhcp_client_get_iface(client));
         }
@@ -628,10 +642,10 @@ stop(NMDhcpClient *client, gboolean release)
 }
 
 static GBytes *
-get_duid(NMDhcpClient *client)
+read_duid_from_lease(NMDhcpDhclient *self)
 {
-    NMDhcpDhclient           *self = NM_DHCP_DHCLIENT(client);
-    NMDhcpDhclientPrivate    *priv = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
+    NMDhcpClient             *client = NM_DHCP_CLIENT(self);
+    NMDhcpDhclientPrivate    *priv   = NM_DHCP_DHCLIENT_GET_PRIVATE(self);
     const NMDhcpClientConfig *client_config;
     GBytes                   *duid      = NULL;
     gs_free char             *leasefile = NULL;
@@ -724,7 +738,6 @@ nm_dhcp_dhclient_class_init(NMDhcpDhclientClass *dhclient_class)
     client_class->ip4_start = ip4_start;
     client_class->ip6_start = ip6_start;
     client_class->stop      = stop;
-    client_class->get_duid  = get_duid;
 }
 
 const NMDhcpClientFactory _nm_dhcp_client_factory_dhclient = {
diff --git a/src/core/dhcp/nm-dhcp-nettools.c b/src/core/dhcp/nm-dhcp-nettools.c
index 05b7b52e..df88362e 100644
--- a/src/core/dhcp/nm-dhcp-nettools.c
+++ b/src/core/dhcp/nm-dhcp-nettools.c
@@ -624,7 +624,7 @@ lease_to_ip4_config(NMDhcpNettools *self, NDhcp4ClientLease *lease, GError **err
 
     l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
-    options = nm_dhcp_option_create_options_dict();
+    options = nm_dhcp_client_create_options_dict(NM_DHCP_CLIENT(self), TRUE);
 
     if (!lease_parse_address(self, lease, l3cd, iface, options, &lease_address, error))
         return NULL;
@@ -1100,7 +1100,7 @@ dhcp4_event_cb(int fd, GIOCondition condition, gpointer user_data)
 }
 
 static gboolean
-nettools_create(NMDhcpNettools *self, GError **error)
+nettools_create(NMDhcpNettools *self, GBytes **out_effective_client_id, GError **error)
 {
     NMDhcpNettoolsPrivate *priv = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
     nm_auto(n_dhcp4_client_config_freep) NDhcp4ClientConfig *config = NULL;
@@ -1197,6 +1197,9 @@ nettools_create(NMDhcpNettools *self, GError **error)
 
     priv->event_source = nm_g_unix_fd_add_source(fd, G_IO_IN, dhcp4_event_cb, self);
 
+    *out_effective_client_id =
+        (client_id == client_id_new) ? g_steal_pointer(&client_id_new) : g_bytes_ref(client_id);
+
     return TRUE;
 }
 
@@ -1287,8 +1290,9 @@ static gboolean
 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);
+    NMDhcpNettools           *self                = NM_DHCP_NETTOOLS(client);
+    NMDhcpNettoolsPrivate    *priv                = NM_DHCP_NETTOOLS_GET_PRIVATE(self);
+    gs_unref_bytes GBytes    *effective_client_id = NULL;
     const NMDhcpClientConfig *client_config;
     gs_free char             *lease_file = NULL;
     struct in_addr            last_addr  = {0};
@@ -1299,7 +1303,7 @@ ip4_start(NMDhcpClient *client, GError **error)
     g_return_val_if_fail(!priv->probe, FALSE);
     g_return_val_if_fail(client_config, FALSE);
 
-    if (!nettools_create(self, error))
+    if (!nettools_create(self, &effective_client_id, error))
         return FALSE;
 
     r = n_dhcp4_client_probe_config_new(&config);
@@ -1445,6 +1449,9 @@ ip4_start(NMDhcpClient *client, GError **error)
     }
 
     _LOGT("dhcp-client4: start " NM_HASH_OBFUSCATE_PTR_FMT, NM_HASH_OBFUSCATE_PTR(priv->client));
+
+    nm_dhcp_client_set_effective_client_id(client, effective_client_id);
+
     return TRUE;
 }
 
diff --git a/src/core/dhcp/nm-dhcp-options.c b/src/core/dhcp/nm-dhcp-options.c
index 96fef81a..a5bb06d1 100644
--- a/src/core/dhcp/nm-dhcp-options.c
+++ b/src/core/dhcp/nm-dhcp-options.c
@@ -190,12 +190,12 @@ static const NMDhcpOption *const _sorted_options_4[G_N_ELEMENTS(_nm_dhcp_option_
 };
 
 const NMDhcpOption _nm_dhcp_option_dhcp6_options[] = {
-    REQ(NM_DHCP_OPTION_DHCP6_CLIENTID, "dhcp6_client_id", FALSE),
+    REQ(NM_DHCP_OPTION_DHCP6_CLIENT_ID, "dhcp6_client_id", FALSE),
 
     /* Don't request server ID by default; some servers don't reply to
      * Information Requests that request the Server ID.
      */
-    REQ(NM_DHCP_OPTION_DHCP6_SERVERID, "dhcp6_server_id", FALSE),
+    REQ(NM_DHCP_OPTION_DHCP6_SERVER_ID, "dhcp6_server_id", FALSE),
 
     REQ(NM_DHCP_OPTION_DHCP6_DNS_SERVERS, "dhcp6_name_servers", TRUE),
     REQ(NM_DHCP_OPTION_DHCP6_DOMAIN_LIST, "dhcp6_domain_search", TRUE),
@@ -460,7 +460,7 @@ nm_dhcp_option_add_requests_to_options(GHashTable *options, int addr_family)
 }
 
 GHashTable *
-nm_dhcp_option_create_options_dict(void)
+nm_dhcp_option_create_options_dict(gboolean static_keys)
 {
-    return g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);
+    return g_hash_table_new_full(nm_str_hash, g_str_equal, static_keys ? NULL : g_free, g_free);
 }
diff --git a/src/core/dhcp/nm-dhcp-options.h b/src/core/dhcp/nm-dhcp-options.h
index 4c978c4f..fcc6f9cd 100644
--- a/src/core/dhcp/nm-dhcp-options.h
+++ b/src/core/dhcp/nm-dhcp-options.h
@@ -157,8 +157,8 @@ typedef enum {
 } NMDhcpOptionDhcp4Options;
 
 typedef enum {
-    NM_DHCP_OPTION_DHCP6_CLIENTID     = 1,
-    NM_DHCP_OPTION_DHCP6_SERVERID     = 2,
+    NM_DHCP_OPTION_DHCP6_CLIENT_ID    = 1,
+    NM_DHCP_OPTION_DHCP6_SERVER_ID    = 2,
     NM_DHCP_OPTION_DHCP6_DNS_SERVERS  = 23,
     NM_DHCP_OPTION_DHCP6_DOMAIN_LIST  = 24,
     NM_DHCP_OPTION_DHCP6_SNTP_SERVERS = 31,
@@ -222,7 +222,8 @@ void nm_dhcp_option_add_option_in_addr(GHashTable *options,
                                        in_addr_t   value);
 void
 nm_dhcp_option_add_option_u64(GHashTable *options, int addr_family, guint option, guint64 value);
-void        nm_dhcp_option_add_requests_to_options(GHashTable *options, int addr_family);
-GHashTable *nm_dhcp_option_create_options_dict(void);
+void nm_dhcp_option_add_requests_to_options(GHashTable *options, int addr_family);
+
+GHashTable *nm_dhcp_option_create_options_dict(gboolean static_keys);
 
 #endif /* __NM_DHCP_OPTIONS_H__ */
diff --git a/src/core/dhcp/nm-dhcp-systemd.c b/src/core/dhcp/nm-dhcp-systemd.c
index 49e21d97..7ce15d30 100644
--- a/src/core/dhcp/nm-dhcp-systemd.c
+++ b/src/core/dhcp/nm-dhcp-systemd.c
@@ -86,7 +86,7 @@ lease_to_ip6_config(NMDhcpSystemd *self, sd_dhcp6_lease *lease, gint32 ts, GErro
 
     l3cd = nm_dhcp_client_create_l3cd(NM_DHCP_CLIENT(self));
 
-    options = nm_dhcp_option_create_options_dict();
+    options = nm_dhcp_client_create_options_dict(NM_DHCP_CLIENT(self), TRUE);
 
     if (!nm_dhcp_client_get_config(NM_DHCP_CLIENT(self))->v6.info_only) {
         gboolean has_any_addresses = FALSE;
@@ -271,7 +271,7 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
 
     /* TODO: honor nm_dhcp_client_get_anycast_address() */
 
-    duid = nm_dhcp_client_get_effective_client_id(client);
+    duid = client_config->client_id;
     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);
@@ -379,6 +379,8 @@ ip6_start(NMDhcpClient *client, const struct in6_addr *ll_addr, GError **error)
         return FALSE;
     }
 
+    nm_dhcp_client_set_effective_client_id(client, duid);
+
     return TRUE;
 }
 
diff --git a/src/core/dhcp/tests/test-dhcp-dhclient.c b/src/core/dhcp/tests/test-dhcp-dhclient.c
index aaa0dbc3..e95f218e 100644
--- a/src/core/dhcp/tests/test-dhcp-dhclient.c
+++ b/src/core/dhcp/tests/test-dhcp-dhclient.c
@@ -895,122 +895,175 @@ test_read_commented_duid_from_leasefile(void)
 /*****************************************************************************/
 
 static void
-_save_duid(const char *path, const guint8 *duid_bin, gsize duid_len)
+_check_duid_impl(const guint8 *duid_bin,
+                 gsize         duid_len,
+                 gboolean      enforce_duid,
+                 const char   *old_content,
+                 const char   *new_content)
 {
-    gs_unref_bytes GBytes *duid  = NULL;
-    GError                *error = NULL;
+    gs_free_error GError  *error    = NULL;
+    gs_free char          *contents = NULL;
     gboolean               success;
+    const char            *path = NM_BUILD_BUILDDIR "/src/core/dhcp/tests/check-duid.lease";
+    gs_unref_bytes GBytes *duid = NULL;
+    gsize                  contents_len;
 
-    g_assert(path);
     g_assert(duid_bin);
     g_assert(duid_len > 0);
 
-    duid    = g_bytes_new(duid_bin, duid_len);
-    success = nm_dhcp_dhclient_save_duid(path, duid, &error);
-    nmtst_assert_success(success, error);
-}
-
-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 =
-        "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n";
-    GError       *error    = NULL;
-    gs_free char *contents = NULL;
-    gboolean      success;
-    const char   *path = "test-dhclient-write-duid.leases";
+    if (!nm_str_is_empty(old_content) || nmtst_get_rand_bool()) {
+        success = g_file_set_contents(path, old_content ?: "", -1, &error);
+        nmtst_assert_success(success, error);
+    } else
+        nmtst_file_unlink_if_exists(path);
 
-    _save_duid(path, duid, G_N_ELEMENTS(duid));
+    duid = g_bytes_new(duid_bin, duid_len);
 
-    success = g_file_get_contents(path, &contents, NULL, &error);
+    success = nm_dhcp_dhclient_save_duid(path, duid, enforce_duid, &error);
     nmtst_assert_success(success, error);
 
-    unlink(path);
-
-    g_assert_cmpstr(expected_contents, ==, contents);
-}
-
-static void
-test_write_existing_duid(void)
-{
-    const guint8 duid[] =
-        {000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302};
-    const char *original_contents =
-        "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;
-    gs_free char *contents = NULL;
-    gboolean      success;
-    const char   *path = "test-dhclient-write-existing-duid.leases";
-
-    success = g_file_set_contents(path, original_contents, -1, &error);
+    success = g_file_get_contents(path, &contents, &contents_len, &error);
     nmtst_assert_success(success, error);
+    g_assert(contents);
 
-    /* Save other DUID; should be overwritten */
-    _save_duid(path, duid, G_N_ELEMENTS(duid));
+    nmtst_file_unlink(path);
 
-    /* reread original contents */
-    success = g_file_get_contents(path, &contents, NULL, &error);
-    nmtst_assert_success(success, error);
+    if (!nm_streq0(new_content, contents))
+        g_error("FAILING:\n\nEXPECTED:\n%s\nACTUAL:\n%s\n\n", new_content, contents);
 
-    unlink(path);
-    g_assert_cmpstr(expected_contents, ==, contents);
+    g_assert_cmpstr(new_content, ==, contents);
+    g_assert_cmpint(contents_len, ==, strlen(contents));
 }
 
-static const guint8 DUID_BIN[] =
-    {000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302};
-#define DUID "\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302"
+#define _DUID(...) ((const guint8[]){__VA_ARGS__})
 
-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;
-    gs_free char *contents          = NULL;
-    gboolean      success;
-    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);
-
-    /* Save other DUID; should be saved on top */
-    _save_duid(path, DUID_BIN, G_N_ELEMENTS(DUID_BIN));
-
-    /* reread original contents */
-    success = g_file_get_contents(path, &contents, NULL, &error);
-    nmtst_assert_success(success, error);
-
-    unlink(path);
-    g_assert_cmpstr(expected_contents, ==, contents);
-#undef ORIG_CONTENTS
-}
+#define _check_duid(duid, enforce_duid, old_content, new_content) \
+    _check_duid_impl((duid), sizeof(duid), (enforce_duid), (old_content), (new_content))
 
 static void
-test_write_existing_multiline_duid(void)
+test_write_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;
-    gboolean                    success;
-    nmtst_auto_unlinkfile char *path =
-        g_strdup("test-dhclient-write-existing-multiline-duid.leases");
-
-    success = g_file_set_contents(path, ORIG_CONTENTS, -1, &error);
-    nmtst_assert_success(success, error);
-
-    _save_duid(path, DUID_BIN, G_N_ELEMENTS(DUID_BIN));
+    _check_duid(_DUID(000, 001, 000, 001, 027, 'X', 0350, 'X', 0, '#', 025, 010, '~', 0254),
+                FALSE,
+                NULL,
+                "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
 
-    success = g_file_get_contents(path, &contents, NULL, &error);
-    nmtst_assert_success(success, error);
+    _check_duid(
+        _DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
+        FALSE,
+        "default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
+        "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n");
 
-    g_assert_cmpstr(expected_contents, ==, contents);
-#undef ORIG_CONTENTS
+    _check_duid(_DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
+                FALSE,
+                "#default-duid \"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
+                "default-duid "
+                "\"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n#default-duid "
+                "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
+    _check_duid(
+        _DUID(000, 001, 000, 001, 023, 'o', 023, 'n', 000, '"', 0372, 0214, 0326, 0302),
+        FALSE,
+        "### Commented old DUID ###\n#default-duid "
+        "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n",
+        "default-duid \"\\000\\001\\000\\001\\023o\\023n\\000\\\"\\372\\214\\326\\302\";\n### "
+        "Commented old DUID ###\n#default-duid "
+        "\"\\000\\001\\000\\001\\027X\\350X\\000#\\025\\010~\\254\";\n");
+
+    _check_duid(
+        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xf),
+        FALSE,
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\n}\n",
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\n}\n");
+
+    _check_duid(
+        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xf),
+        FALSE,
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\r\n}\n",
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\r\n}\n");
+
+    _check_duid(
+        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xe),
+        FALSE,
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\r\n}\n",
+        "default-duid \"\\252\\013\\314\\015\\356\\016\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\r\n}\n");
+
+    _check_duid(
+        _DUID(0xaa, 0xb, 0xcc, 0xd, 0xee, 0xe),
+        TRUE,
+        "default-duid \"\\252\\013\\314\\015\\356\\017\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:f;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\n}\n",
+        "default-duid \"\\252\\013\\314\\015\\356\\016\";\nlease6 {\n  interface \"eth1\";\n "
+        " ia-na f1:ce:00:01 {\n    starts 1671015678;\n    renew 60;\n    rebind 105;\n    "
+        "iaaddr 192:168:121::1:112c {\n      starts 1671015678;\n      preferred-life 120;\n "
+        "     max-life 120;\n    }\n  }\n  option fqdn.encoded true;\n  option "
+        "fqdn.server-update true;\n  option fqdn.no-client-update false;\n  option fqdn.fqdn "
+        "\"dff6de4fcb0f\";\n  option fqdn.hostname \"dff6de4fcb0f\";\n  option dhcp6.client-id "
+        "aa:b:cc:d:ee:e;\n  option dhcp6.server-id 0:1:0:1:2b:2c:4d:1d:0:0:0:0:0:0;\n  option "
+        "dhcp6.name-servers 192:168:121:0:ce0f:f1ff:fece:1;\n  option dhcp6.fqdn "
+        "1:c:64:66:66:36:64:65:34:66:63:62:30:66;\n  option dhcp6.status-code success "
+        "\"success\";\n}\n");
 }
 
 /*****************************************************************************/
@@ -1329,12 +1382,7 @@ main(int argc, char **argv)
     g_test_add_func("/dhcp/dhclient/read_commented_duid_from_leasefile",
                     test_read_commented_duid_from_leasefile);
 
-    g_test_add_func("/dhcp/dhclient/write_duid", test_write_duid);
-    g_test_add_func("/dhcp/dhclient/write_existing_duid", test_write_existing_duid);
-    g_test_add_func("/dhcp/dhclient/write_existing_commented_duid",
-                    test_write_existing_commented_duid);
-    g_test_add_func("/dhcp/dhclient/write_existing_multiline_duid",
-                    test_write_existing_multiline_duid);
+    g_test_add_func("/dhcp/dhclient/test_write_duid", test_write_duid);
 
     return g_test_run();
 }