summary refs log tree commit diff
path: root/src/nm-core-utils.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nm-core-utils.c')
-rw-r--r--src/nm-core-utils.c984
1 files changed, 737 insertions, 247 deletions
diff --git a/src/nm-core-utils.c b/src/nm-core-utils.c
index ca8c9526..a65ac636 100644
--- a/src/nm-core-utils.c
+++ b/src/nm-core-utils.c
@@ -30,15 +30,19 @@
 #include <unistd.h>
 #include <stdlib.h>
 #include <resolv.h>
+#include <byteswap.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <sys/stat.h>
 #include <linux/if.h>
 #include <linux/if_infiniband.h>
+#include <net/if_arp.h>
 #include <net/ethernet.h>
 
 #include "nm-utils/nm-random-utils.h"
 #include "nm-utils/nm-io-utils.h"
+#include "nm-utils/unaligned.h"
+#include "nm-utils/nm-secret-utils.h"
 #include "nm-utils.h"
 #include "nm-core-internal.h"
 #include "nm-setting-connection.h"
@@ -48,6 +52,17 @@
 #include "nm-setting-wireless-security.h"
 
 G_STATIC_ASSERT (sizeof (NMUtilsTestFlags) <= sizeof (int));
+
+/* we read _nm_utils_testing without memory barrier. This is thread-safe,
+ * because the static variable is initialized to zero, and only reset
+ * once to a non-zero value (via g_atomic_int_compare_and_exchange()).
+ *
+ * Since there is only one integer that contains the data, there is no
+ * caching problem reading this (atomic int) variable without
+ * synchronization/memory-barrier. Contrary to a double-checked locking,
+ * where one needs a memory barrier to read the variable and ensure
+ * that also the related data is coherent in cache. Here there is no
+ * related data. */
 static int _nm_utils_testing = 0;
 
 gboolean
@@ -66,6 +81,7 @@ nm_utils_get_testing ()
 {
 	NMUtilsTestFlags flags;
 
+again:
 	flags = (NMUtilsTestFlags) _nm_utils_testing;
 	if (flags != NM_UTILS_TEST_NONE) {
 		/* Flags already initialized. Return them. */
@@ -78,12 +94,11 @@ nm_utils_get_testing ()
 	if (g_test_initialized ())
 		flags |= _NM_UTILS_TEST_GENERAL;
 
-	if (g_atomic_int_compare_and_exchange (&_nm_utils_testing, 0, (int) flags)) {
-		/* Done. We set it. */
-		return flags & NM_UTILS_TEST_ALL;
-	}
-	/* It changed in the meantime (??). Re-read the value. */
-	return ((NMUtilsTestFlags) _nm_utils_testing) & NM_UTILS_TEST_ALL;
+	g_atomic_int_compare_and_exchange (&_nm_utils_testing, 0, (int) flags);
+
+	/* regardless of whether we won the race of initializing _nm_utils_testing,
+	 * go back and read the value again. It must be non-zero by now. */
+	goto again;
 }
 
 void
@@ -1133,6 +1148,7 @@ nm_utils_read_link_absolute (const char *link_file, GError **error)
 #define DEVICE_TYPE_TAG "type:"
 #define DRIVER_TAG "driver:"
 #define SUBCHAN_TAG "s390-subchannels:"
+#define DHCP_PLUGIN_TAG "dhcp-plugin:"
 #define EXCEPT_TAG "except:"
 #define MATCH_TAG_CONFIG_NM_VERSION             "nm-version:"
 #define MATCH_TAG_CONFIG_NM_VERSION_MIN         "nm-version-min:"
@@ -1144,6 +1160,7 @@ typedef struct {
 	const char *device_type;
 	const char *driver;
 	const char *driver_version;
+	const char *dhcp_plugin;
 	struct {
 		const char *value;
 		gboolean is_parsed;
@@ -1280,6 +1297,34 @@ match_device_hwaddr_eval (const char *spec_str,
 		_has; \
 	})
 
+static NMMatchSpecMatchType
+_match_result (gboolean has_except,
+               gboolean has_not_except,
+               gboolean has_match,
+               gboolean has_match_except)
+{
+	if (   has_except
+	    && !has_not_except) {
+		/* a match spec that only consists of a list of except matches is treated specially. */
+		nm_assert (!has_match);
+		if (has_match_except) {
+			/* one of the "except:" matches matched. The result is an explicit
+			 * negative match. */
+			return NM_MATCH_SPEC_NEG_MATCH;
+		} else {
+			/* none of the "except:" matches matched. The result is a positive match,
+			 * despite there being no positive match. */
+			return NM_MATCH_SPEC_MATCH;
+		}
+	}
+
+	if (has_match_except)
+		return NM_MATCH_SPEC_NEG_MATCH;
+	if (has_match)
+		return NM_MATCH_SPEC_MATCH;
+	return NM_MATCH_SPEC_NO_MATCH;
+}
+
 static const char *
 match_except (const char *spec_str, gboolean *out_except)
 {
@@ -1361,6 +1406,9 @@ match_device_eval (const char *spec_str,
 	if (_MATCH_CHECK (spec_str, SUBCHAN_TAG))
 		return match_data_s390_subchannels_eval (spec_str, match_data);
 
+	if (_MATCH_CHECK (spec_str, DHCP_PLUGIN_TAG))
+		return nm_streq0 (spec_str, match_data->dhcp_plugin);
+
 	if (allow_fuzzy) {
 		if (match_device_hwaddr_eval (spec_str, match_data))
 			return TRUE;
@@ -1379,17 +1427,21 @@ nm_match_spec_device (const GSList *specs,
                       const char *driver,
                       const char *driver_version,
                       const char *hwaddr,
-                      const char *s390_subchannels)
+                      const char *s390_subchannels,
+                      const char *dhcp_plugin)
 {
 	const GSList *iter;
-	NMMatchSpecMatchType match;
+	gboolean has_match = FALSE;
+	gboolean has_match_except = FALSE;
+	gboolean has_except = FALSE;
+	gboolean has_not_except = FALSE;
 	const char *spec_str;
-	gboolean except;
 	MatchDeviceData match_data = {
 	    .interface_name = interface_name,
 	    .device_type = nm_str_not_empty (device_type),
 	    .driver = nm_str_not_empty (driver),
 	    .driver_version = nm_str_not_empty (driver_version),
+	    .dhcp_plugin = nm_str_not_empty (dhcp_plugin),
 	    .hwaddr = {
 	        .value = hwaddr,
 	    },
@@ -1403,19 +1455,9 @@ nm_match_spec_device (const GSList *specs,
 	if (!specs)
 		return NM_MATCH_SPEC_NO_MATCH;
 
-	match = NM_MATCH_SPEC_NO_MATCH;
-
-	/* pre-search for "*" */
 	for (iter = specs; iter; iter = iter->next) {
-		spec_str = iter->data;
-
-		if (spec_str && spec_str[0] == '*' && spec_str[1] == '\0') {
-			match = NM_MATCH_SPEC_MATCH;
-			break;
-		}
-	}
+		gboolean except;
 
-	for (iter = specs; iter; iter = iter->next) {
 		spec_str = iter->data;
 
 		if (!spec_str || !*spec_str)
@@ -1423,10 +1465,14 @@ nm_match_spec_device (const GSList *specs,
 
 		spec_str = match_except (spec_str, &except);
 
-		if (   !except
-		    && match == NM_MATCH_SPEC_MATCH) {
-			/* we have no "except-match" but already match. No need to evaluate
-			 * the match, we cannot match stronger. */
+		if (except)
+			has_except = TRUE;
+		else
+			has_not_except = TRUE;
+
+		if (   ( except && has_match_except)
+		    || (!except && has_match)) {
+			/* evaluating the match does not give new information. Skip it. */
 			continue;
 		}
 
@@ -1436,11 +1482,12 @@ nm_match_spec_device (const GSList *specs,
 			continue;
 
 		if (except)
-			return NM_MATCH_SPEC_NEG_MATCH;
-		match = NM_MATCH_SPEC_MATCH;
+			has_match_except = TRUE;
+		else
+			has_match = TRUE;
 	}
 
-	return match;
+	return _match_result (has_except, has_not_except, has_match, has_match_except);
 }
 
 static gboolean
@@ -1510,7 +1557,10 @@ NMMatchSpecMatchType
 nm_match_spec_config (const GSList *specs, guint cur_nm_version, const char *env)
 {
 	const GSList *iter;
-	NMMatchSpecMatchType match = NM_MATCH_SPEC_NO_MATCH;
+	gboolean has_match = FALSE;
+	gboolean has_match_except = FALSE;
+	gboolean has_except = FALSE;
+	gboolean has_not_except = FALSE;
 
 	if (!specs)
 		return NM_MATCH_SPEC_NO_MATCH;
@@ -1525,6 +1575,17 @@ nm_match_spec_config (const GSList *specs, guint cur_nm_version, const char *env
 
 		spec_str = match_except (spec_str, &except);
 
+		if (except)
+			has_except = TRUE;
+		else
+			has_not_except = TRUE;
+
+		if (   ( except && has_match_except)
+		    || (!except && has_match)) {
+			/* evaluating the match does not give new information. Skip it. */
+			continue;
+		}
+
 		if (_MATCH_CHECK (spec_str, MATCH_TAG_CONFIG_NM_VERSION))
 			v_match = match_config_eval (spec_str, MATCH_TAG_CONFIG_NM_VERSION, cur_nm_version);
 		else if (_MATCH_CHECK (spec_str, MATCH_TAG_CONFIG_NM_VERSION_MIN))
@@ -1534,15 +1595,18 @@ nm_match_spec_config (const GSList *specs, guint cur_nm_version, const char *env
 		else if (_MATCH_CHECK (spec_str, MATCH_TAG_CONFIG_ENV))
 			v_match = env && env[0] && !strcmp (spec_str, env);
 		else
+			v_match = FALSE;
+
+		if (!v_match)
 			continue;
 
-		if (v_match) {
-			if (except)
-				return NM_MATCH_SPEC_NEG_MATCH;
-			match = NM_MATCH_SPEC_MATCH;
-		}
+		if (except)
+			has_match_except = TRUE;
+		else
+			has_match = TRUE;
 	}
-	return match;
+
+	return _match_result (has_except, has_not_except, has_match, has_match_except);
 }
 
 #undef _MATCH_CHECK
@@ -2503,209 +2567,549 @@ nm_utils_is_specific_hostname (const char *name)
 
 /*****************************************************************************/
 
-gboolean
-nm_utils_machine_id_parse (const char *id_str, /*uuid_t*/ guchar *out_uuid)
-{
-	int i;
-	guint8 v0, v1;
-
-	if (!id_str)
-		return FALSE;
-
-	for (i = 0; i < 32; i++) {
-		if (!g_ascii_isxdigit (id_str[i]))
-			return FALSE;
-	}
-	if (id_str[i] != '\0')
-		return FALSE;
+typedef struct {
+	NMUuid bin;
+	char _nul_sentinel; /* just for safety, if somebody accidentally uses the binary in a string context. */
 
-	if (out_uuid) {
-		for (i = 0; i < 16; i++) {
-			v0 = g_ascii_xdigit_value (*(id_str++));
-			v1 = g_ascii_xdigit_value (*(id_str++));
-			out_uuid[i] = (v0 << 4) + v1;
-		}
+	/* depending on whether the string is packed or not (with/without hyphens),
+	 * it's 32 or 36 characters long (plus the trailing NUL).
+	 *
+	 * The difference is that boot-id is a valid RFC 4211 UUID and represented
+	 * as a 36 ascii string (with hyphens). The machine-id technically is not
+	 * a UUID, but just a 32 byte sequence of hexchars. */
+	char str[37];
+	bool is_fake;
+} UuidData;
+
+static UuidData *
+_uuid_data_init (UuidData *uuid_data,
+                 gboolean packed,
+                 gboolean is_fake,
+                 const NMUuid *uuid)
+{
+	nm_assert (uuid_data);
+	nm_assert (uuid);
+
+	uuid_data->bin = *uuid;
+	uuid_data->_nul_sentinel = '\0';
+	uuid_data->is_fake = is_fake;
+	if (packed) {
+		G_STATIC_ASSERT_EXPR (sizeof (uuid_data->str) >= (sizeof (*uuid) * 2 + 1));
+		_nm_utils_bin2hexstr_full (uuid,
+		                           sizeof (*uuid),
+		                           '\0',
+		                           FALSE,
+		                           uuid_data->str);
+	} else {
+		G_STATIC_ASSERT_EXPR (sizeof (uuid_data->str) >= 37);
+		_nm_utils_uuid_unparse (uuid, uuid_data->str);
 	}
-	return TRUE;
+	return uuid_data;
 }
 
-char *
-nm_utils_machine_id_read (void)
+/*****************************************************************************/
+
+static const UuidData *
+_machine_id_get (gboolean allow_fake)
 {
-	gs_free char *contents = NULL;
-	int i;
+	static const UuidData *volatile p_uuid_data;
+	const UuidData *d;
+
+again:
+	d = g_atomic_pointer_get (&p_uuid_data);
+	if (G_UNLIKELY (!d)) {
+		static gsize lock;
+		static UuidData uuid_data;
+		gs_free char *content = NULL;
+		gboolean is_fake = TRUE;
+		const char *fake_type = NULL;
+		NMUuid uuid;
+
+		/* Get the machine ID from /etc/machine-id; it's always in /etc no matter
+		 * where our configured SYSCONFDIR is.  Alternatively, it might be in
+		 * LOCALSTATEDIR /lib/dbus/machine-id.
+		 */
+		if (   nm_utils_file_get_contents (-1, "/etc/machine-id", 100*1024, 0, &content, NULL, NULL) >= 0
+		    || nm_utils_file_get_contents (-1, LOCALSTATEDIR"/lib/dbus/machine-id", 100*1024, 0, &content, NULL, NULL) >= 0) {
+			g_strstrip (content);
+			if (_nm_utils_hexstr2bin_full (content,
+			                               FALSE,
+			                               FALSE,
+			                               NULL,
+			                               16,
+			                               (guint8 *) &uuid,
+			                               sizeof (uuid),
+			                               NULL)) {
+				if (!nm_utils_uuid_is_null (&uuid)) {
+					/* an all-zero machine-id is not valid. */
+					is_fake = FALSE;
+				}
+			}
+		}
 
-	/* Get the machine ID from /etc/machine-id; it's always in /etc no matter
-	 * where our configured SYSCONFDIR is.  Alternatively, it might be in
-	 * LOCALSTATEDIR /lib/dbus/machine-id.
-	 */
-	if (   !g_file_get_contents ("/etc/machine-id", &contents, NULL, NULL)
-	    && !g_file_get_contents (LOCALSTATEDIR "/lib/dbus/machine-id", &contents, NULL, NULL))
-		return NULL;
+		if (is_fake) {
+			const guint8 *seed_bin;
+			const char *hash_seed;
+			gsize seed_len;
+
+			if (!allow_fake) {
+				/* we don't allow generating (and memoizing) a fake key.
+				 * Signal that no valid machine-id exists. */
+				return NULL;
+			}
 
-	contents = g_strstrip (contents);
+			if (nm_utils_host_id_get (&seed_bin, &seed_len)) {
+				/* we have no valid machine-id. Generate a fake one by hashing
+				 * the secret-key. This key is commonly persisted, so it should be
+				 * stable accross reboots (despite having a broken system without
+				 * proper machine-id). */
+				fake_type = "secret-key";
+				hash_seed = "ab085f06-b629-46d1-a553-84eeba5683b6";
+			} else {
+				/* the secret-key is not valid/persistent either. That happens when we fail
+				 * to read/write the secret-key to disk. Fallback to boot-id. The boot-id
+				 * itself may be fake and randomly generated ad-hoc, but that is as best
+				 * as it gets.  */
+				seed_bin = (const guint8 *) nm_utils_boot_id_bin ();
+				seed_len = sizeof (NMUuid);
+				fake_type = "boot-id";
+				hash_seed = "7ff0c8f5-5399-4901-ab63-61bf594abe8b";
+			}
 
-	for (i = 0; i < 32; i++) {
-		if (!g_ascii_isxdigit (contents[i]))
-			return NULL;
-		if (contents[i] >= 'A' && contents[i] <= 'F') {
-			/* canonicalize to lower-case */
-			contents[i] = 'a' + (contents[i] - 'A');
+			/* the fake machine-id is based on secret-key/boot-id, but we hash it
+			 * again, so that they are not literally the same. */
+			nm_utils_uuid_generate_from_string_bin (&uuid,
+			                                        (const char *) seed_bin,
+			                                        seed_len,
+			                                        NM_UTILS_UUID_TYPE_VERSION5,
+			                                        (gpointer) hash_seed);
 		}
+
+		if (!g_once_init_enter (&lock))
+			goto again;
+
+		d = _uuid_data_init (&uuid_data, TRUE, is_fake, &uuid);
+		g_atomic_pointer_set (&p_uuid_data, d);
+		g_once_init_leave (&lock, 1);
+
+		if (is_fake) {
+			nm_log_err (LOGD_CORE,
+			            "/etc/machine-id: no valid machine-id. Use fake one based on %s: %s",
+			            fake_type,
+			            d->str);
+		} else
+			nm_log_dbg (LOGD_CORE, "/etc/machine-id: %s", d->str);
 	}
-	if (contents[i] != '\0')
-		return NULL;
 
-	return g_steal_pointer (&contents);
+	return d;
+}
+
+const char *
+nm_utils_machine_id_str (void)
+{
+	return _machine_id_get (TRUE)->str;
+}
+
+const NMUuid *
+nm_utils_machine_id_bin (void)
+{
+	return &_machine_id_get (TRUE)->bin;
+}
+
+gboolean
+nm_utils_machine_id_is_fake (void)
+{
+	return _machine_id_get (TRUE)->is_fake;
 }
 
 /*****************************************************************************/
 
+/* prefix for version2 secret key. The secret key is hashed with /etc/machine-id. */
+#define SECRET_KEY_V2_PREFIX "nm-v2:"
+#define SECRET_KEY_FILE      NMSTATEDIR"/secret_key"
+
 static gboolean
-_secret_key_read (guint8 **out_secret_key,
-                  gsize *out_key_len)
+_host_id_read_timestamp (gboolean use_secret_key_file,
+                         const guint8 *host_id,
+                         gsize host_id_len,
+                         gint64 *out_timestamp_ns)
 {
-	guint8 *secret_key;
-	gboolean success = TRUE;
-	gsize key_len;
-	gs_free_error GError *error = NULL;
+	struct stat st;
+	gint64 now;
+	guint64 v;
+
+	if (   use_secret_key_file
+	    && stat (SECRET_KEY_FILE, &st) == 0) {
+		/* don't check for overflow or timestamps in the future. We get whatever
+		 * (bogus) date is on the file. */
+		*out_timestamp_ns = (st.st_mtim.tv_sec * NM_UTILS_NS_PER_SECOND) + st.st_mtim.tv_nsec;
+		return TRUE;
+	}
 
-	/* Let's try to load a saved secret key first. */
-	if (g_file_get_contents (NMSTATEDIR "/secret_key", (char **) &secret_key, &key_len, &error)) {
-		if (key_len >= 16)
-			goto out;
+	/* generate a fake timestamp based on the host-id.
+	 *
+	 * This really should never happen under normal circumstances. We already
+	 * are in a code path, where the system has a problem (unable to get good randomness
+	 * and/or can't access the secret_key). In such a scenario, a fake timestamp is the
+	 * least of our problems.
+	 *
+	 * At least, generate something sensible so we don't have to worry about the
+	 * timestamp. It is wrong to worry about using a fake timestamp (which is tied to
+	 * the secret_key) if we are unable to access the secret_key file in the first place.
+	 *
+	 * Pick a random timestamp from the past two years. Yes, this timestamp
+	 * is not stable accross restarts, but apparently neither is the host-id
+	 * nor the secret_key itself. */
 
-		/* the secret key is borked. Log a warning, but proceed below to generate
-		 * a new one. */
-		nm_log_warn (LOGD_CORE, "secret-key: too short secret key in \"%s\" (generate new key)", NMSTATEDIR "/secret_key");
-		nm_clear_g_free (&secret_key);
-	} else {
-		if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+#define EPOCH_TWO_YEARS  (G_GINT64_CONSTANT (2 * 365 * 24 * 3600) * NM_UTILS_NS_PER_SECOND)
+
+	v = nm_hash_siphash42 (1156657133u, host_id, host_id_len);
+
+	now = time (NULL);
+	*out_timestamp_ns = NM_MAX ((gint64) 1,
+	                            (now * NM_UTILS_NS_PER_SECOND) - ((gint64) (v % ((guint64) (EPOCH_TWO_YEARS)))));
+	return FALSE;
+}
+
+static const guint8 *
+_host_id_hash_v2 (const guint8 *seed_arr,
+                  gsize seed_len,
+                  guint8 *out_digest /* 32 bytes (NM_UTILS_CHECKSUM_LENGTH_SHA256) */)
+{
+	nm_auto_free_checksum GChecksum *sum = g_checksum_new (G_CHECKSUM_SHA256);
+	const UuidData *machine_id_data;
+	char slen[100];
+
+	/*
+	    (stat -c '%s' /var/lib/NetworkManager/secret_key;
+	     echo -n ' ';
+	     cat /var/lib/NetworkManager/secret_key;
+	     cat /etc/machine-id | tr -d '\n' | sed -n 's/[a-f0-9-]/\0/pg') | sha256sum
+	*/
+
+	nm_sprintf_buf (slen, "%"G_GSIZE_FORMAT" ", seed_len);
+	g_checksum_update (sum, (const guchar *) slen, strlen (slen));
+
+	g_checksum_update (sum, (const guchar *) seed_arr, seed_len);
+
+	machine_id_data = _machine_id_get (FALSE);
+	if (   machine_id_data
+	    && !machine_id_data->is_fake)
+		g_checksum_update (sum, (const guchar *) machine_id_data->str, strlen (machine_id_data->str));
+
+	nm_utils_checksum_get_digest_len (sum, out_digest, NM_UTILS_CHECKSUM_LENGTH_SHA256);
+	return out_digest;
+}
+
+static gboolean
+_host_id_read (guint8 **out_host_id,
+               gsize *out_host_id_len)
+{
+#define SECRET_KEY_LEN 32u
+	guint8 sha256_digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
+	nm_auto_clear_secret_ptr NMSecretPtr file_content = { 0 };
+	const guint8 *secret_arr;
+	gsize secret_len;
+	GError *error = NULL;
+	gboolean success;
+
+	if (nm_utils_file_get_contents (-1,
+	                                SECRET_KEY_FILE,
+	                                10*1024,
+	                                NM_UTILS_FILE_GET_CONTENTS_FLAG_SECRET,
+	                                (char **) &file_content.str,
+	                                &file_content.len,
+	                                &error) < 0) {
+		if (!nm_utils_error_is_notfound (error)) {
 			nm_log_warn (LOGD_CORE, "secret-key: failure reading secret key in \"%s\": %s (generate new key)",
-			             NMSTATEDIR "/secret_key", error->message);
+			             SECRET_KEY_FILE, error->message);
 		}
 		g_clear_error (&error);
-	}
-
-	/* RFC7217 mandates the key SHOULD be at least 128 bits.
-	 * Let's use twice as much. */
-	key_len = 32;
-	secret_key = g_malloc (key_len + 1);
-
-	/* the secret-key is binary. Still, ensure that it's NULL terminated, just like
-	 * g_file_set_contents() does. */
-	secret_key[32] = '\0';
+	} else if (   file_content.len >= NM_STRLEN (SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN
+	           && memcmp (file_content.bin, SECRET_KEY_V2_PREFIX, NM_STRLEN (SECRET_KEY_V2_PREFIX)) == 0) {
+		/* for this type of secret key, we require a prefix followed at least SECRET_KEY_LEN (32) bytes. We
+		 * (also) do that, because older versions of NetworkManager wrote exactly 32 bytes without
+		 * prefix, so we won't wrongly interpret such legacy keys as v2 (if they accidentally have
+		 * a SECRET_KEY_V2_PREFIX prefix, they'll still have the wrong size).
+		 *
+		 * Note that below we generate the random seed in base64 encoding. But that is only done
+		 * to write an ASCII file. There is no base64 decoding and the ASCII is hashed as-is.
+		 * We would accept any binary data just as well (provided a suitable prefix and at least
+		 * 32 bytes).
+		 *
+		 * Note that when hashing the v2 content, we also hash the prefix. There is no strong reason,
+		 * except that it seems simpler not to distinguish between the v2 prefix and the content.
+		 * It's all just part of the seed. */
 
-	if (!nm_utils_random_bytes (secret_key, key_len)) {
-		nm_log_warn (LOGD_CORE, "secret-key: failure to generate good random data for secret-key (use non-persistent key)");
-		success = FALSE;
+		secret_arr = _host_id_hash_v2 (file_content.bin, file_content.len, sha256_digest);
+		secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
+		success = TRUE;
 		goto out;
+	} else if (file_content.len >= 16) {
+		secret_arr = file_content.bin;
+		secret_len = file_content.len;
+		success = TRUE;
+		goto out;
+	} else {
+		/* the secret key is borked. Log a warning, but proceed below to generate
+		 * a new one. */
+		nm_log_warn (LOGD_CORE, "secret-key: too short secret key in \"%s\" (generate new key)", SECRET_KEY_FILE);
 	}
 
-	if (!nm_utils_file_set_contents (NMSTATEDIR "/secret_key", (char *) secret_key, key_len, 0077, &error)) {
-		nm_log_warn (LOGD_CORE, "secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)",
-		             NMSTATEDIR "/secret_key", error->message);
-		success = FALSE;
-		goto out;
+	/* generate and persist new key */
+	{
+#define SECRET_KEY_LEN_BASE64 ((((SECRET_KEY_LEN / 3) + 1) * 4) + 4)
+		guint8 rnd_buf[SECRET_KEY_LEN];
+		guint8 new_content[NM_STRLEN (SECRET_KEY_V2_PREFIX) + SECRET_KEY_LEN_BASE64];
+		int base64_state = 0;
+		int base64_save = 0;
+		gsize len;
+
+		success = nm_utils_random_bytes (rnd_buf, sizeof (rnd_buf));
+
+		/* Our key is really binary data. But since we anyway generate a random seed
+		 * (with 32 random bytes), don't write it in binary, but instead create
+		 * an pure ASCII (base64) representation. Note that the ASCII will still be taken
+		 * as-is (no base64 decoding is done). The sole purpose is to write a ASCII file
+		 * instead of a binary. The content is gibberish either way. */
+		memcpy (new_content, SECRET_KEY_V2_PREFIX, NM_STRLEN (SECRET_KEY_V2_PREFIX));
+		len = NM_STRLEN (SECRET_KEY_V2_PREFIX);
+		len += g_base64_encode_step (rnd_buf,
+		                             sizeof (rnd_buf),
+		                             FALSE,
+		                             (char *) &new_content[len],
+		                             &base64_state,
+		                             &base64_save);
+		len += g_base64_encode_close (FALSE,
+		                              (char *) &new_content[len],
+		                              &base64_state,
+		                              &base64_save);
+		nm_assert (len <= sizeof (new_content));
+
+		secret_arr = _host_id_hash_v2 (new_content, len, sha256_digest);
+		secret_len = NM_UTILS_CHECKSUM_LENGTH_SHA256;
+
+		if (!success)
+			nm_log_warn (LOGD_CORE, "secret-key: failure to generate good random data for secret-key (use non-persistent key)");
+		else if (nm_utils_get_testing ()) {
+			/* for test code, we don't write the generated secret-key to disk. */
+		} else if (!nm_utils_file_set_contents (SECRET_KEY_FILE,
+		                                        (const char *) new_content,
+		                                        len,
+		                                        0077,
+		                                        &error)) {
+			nm_log_warn (LOGD_CORE, "secret-key: failure to persist secret key in \"%s\" (%s) (use non-persistent key)",
+			             SECRET_KEY_FILE, error->message);
+			g_clear_error (&error);
+			success = FALSE;
+		} else
+			nm_log_dbg (LOGD_CORE, "secret-key: persist new secret key to \"%s\"", SECRET_KEY_FILE);
+
+		nm_explicit_bzero (rnd_buf, sizeof (rnd_buf));
+		nm_explicit_bzero (new_content, sizeof (new_content));
 	}
 
 out:
-	/* regardless of success or failue, we always return a secret-key. The
-	 * caller may choose to ignore the error and proceed. */
-	*out_key_len = key_len;
-	*out_secret_key = secret_key;
+	*out_host_id_len = secret_len;
+	*out_host_id = nm_memdup (secret_arr, secret_len);
 	return success;
 }
 
 typedef struct {
-	const guint8 *secret_key;
-	gsize key_len;
+	guint8 *host_id;
+	gsize host_id_len;
+	gint64 timestamp_ns;
 	bool is_good:1;
-} SecretKeyData;
+	bool timestamp_is_good:1;
+} HostIdData;
 
-gboolean
-nm_utils_secret_key_get (const guint8 **out_secret_key,
-                         gsize *out_key_len)
+static const HostIdData *
+_host_id_get (void)
 {
-	static volatile const SecretKeyData *secret_key_static;
-	const SecretKeyData *secret_key;
+	static const HostIdData *volatile host_id_static;
+	const HostIdData *host_id;
 
-	secret_key = g_atomic_pointer_get (&secret_key_static);
-	if (G_UNLIKELY (!secret_key)) {
+again:
+	host_id = g_atomic_pointer_get (&host_id_static);
+	if (G_UNLIKELY (!host_id)) {
+		static HostIdData host_id_data;
 		static gsize init_value = 0;
-		static SecretKeyData secret_key_data;
-		gboolean tmp_success;
-		gs_free guint8 *tmp_secret_key = NULL;
-		gsize tmp_key_len;
-
-		tmp_success = _secret_key_read (&tmp_secret_key, &tmp_key_len);
-		if (g_once_init_enter (&init_value)) {
-			secret_key_data.secret_key = tmp_secret_key;
-			secret_key_data.key_len = tmp_key_len;
-			secret_key_data.is_good = tmp_success;
-
-			if (g_atomic_pointer_compare_and_exchange (&secret_key_static, NULL, &secret_key_data)) {
-				g_steal_pointer (&tmp_secret_key);
-				secret_key = &secret_key_data;
-			}
 
-			g_once_init_leave (&init_value, 1);
-		}
-		if (!secret_key)
-			secret_key = g_atomic_pointer_get (&secret_key_static);
+		if (!g_once_init_enter (&init_value))
+			goto again;
+
+		host_id_data.is_good = _host_id_read (&host_id_data.host_id,
+		                                      &host_id_data.host_id_len);
+
+		host_id_data.timestamp_is_good = _host_id_read_timestamp (host_id_data.is_good,
+		                                                          host_id_data.host_id,
+		                                                          host_id_data.host_id_len,
+		                                                          &host_id_data.timestamp_ns);
+		if (   !host_id_data.timestamp_is_good
+		    && host_id_data.is_good)
+			nm_log_warn (LOGD_CORE, "secret-key: failure reading host timestamp (use fake one)");
+
+		host_id = &host_id_data;
+		g_atomic_pointer_set (&host_id_static, host_id);
+		g_once_init_leave (&init_value, 1);
 	}
 
-	*out_secret_key = secret_key->secret_key;
-	*out_key_len = secret_key->key_len;
-	return secret_key->is_good;
+	return host_id;
 }
 
-gint64
-nm_utils_secret_key_get_timestamp (void)
+/**
+ * nm_utils_host_id_get:
+ * @out_host_id: (out) (transfer none): the binary host key
+ * @out_host_id_len: the length of the host key.
+ *
+ * This returns a per-host key that depends on /var/lib/NetworkManage/secret_key
+ * and (depending on the version) on /etc/machine-id. If /var/lib/NetworkManage/secret_key
+ * does not exist, it will be generated and persisted for next boot.
+ *
+ * Returns: %TRUE, if the host key is "good". Note that this function
+ *   will always succeed to return a host-key, and that this key
+ *   won't change during the run of the program (no matter what).
+ *   A %FALSE return possibly means, that the secret_key is not persisted
+ *   to disk, and/or that it was generated with bad randomness.
+ */
+gboolean
+nm_utils_host_id_get (const guint8 **out_host_id,
+                      gsize *out_host_id_len)
 {
-	struct stat stat_buf;
-	const guint8 *key;
-	gsize key_len;
+	const HostIdData *host_id;
 
-	if (!nm_utils_secret_key_get (&key, &key_len))
-		return 0;
-
-	if (stat (NMSTATEDIR "/secret_key", &stat_buf) != 0)
-		return 0;
+	host_id = _host_id_get ();
+	*out_host_id = host_id->host_id;
+	*out_host_id_len = host_id->host_id_len;
+	return host_id->is_good;
+}
 
-	return stat_buf.st_mtim.tv_sec;
+gint64
+nm_utils_host_id_get_timestamp_ns (void)
+{
+	return _host_id_get ()->timestamp_ns;
 }
 
 /*****************************************************************************/
 
-const char *
-nm_utils_get_boot_id (void)
+static const UuidData *
+_boot_id_get (void)
 {
-	static const char *boot_id;
-
-	if (G_UNLIKELY (!boot_id)) {
+	static const UuidData *volatile p_boot_id;
+	const UuidData *d;
+
+again:
+	d = g_atomic_pointer_get (&p_boot_id);
+	if (G_UNLIKELY (!d)) {
+		static gsize lock;
+		static UuidData boot_id;
 		gs_free char *contents = NULL;
+		NMUuid uuid;
+		gboolean is_fake = FALSE;
 
 		nm_utils_file_get_contents (-1, "/proc/sys/kernel/random/boot_id", 0,
 		                            NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
 		                            &contents, NULL, NULL);
-		if (contents) {
-			g_strstrip (contents);
-			if (contents[0]) {
-				/* clone @contents because we keep @boot_id until the program
-				 * ends.
-				 * nm_utils_file_get_contents() likely allocated a larger
-				 * buffer chunk initially and (although using realloc to shrink
-				 * the buffer) it might not be best to keep this memory
-				 * around. */
-				boot_id = g_strdup (contents);
-			}
+		if (   !contents
+		    || !_nm_utils_uuid_parse (nm_strstrip (contents), &uuid)) {
+			/* generate a random UUID instead. */
+			is_fake = TRUE;
+			_nm_utils_uuid_generate_random (&uuid);
 		}
-		if (!boot_id)
-			boot_id = nm_utils_uuid_generate ();
+
+		if (!g_once_init_enter (&lock))
+			goto again;
+
+		d = _uuid_data_init (&boot_id, FALSE, is_fake, &uuid);
+		g_atomic_pointer_set (&p_boot_id, d);
+		g_once_init_leave (&lock, 1);
 	}
 
-	return boot_id;
+	return d;
+}
+
+const char *
+nm_utils_boot_id_str (void)
+{
+	return _boot_id_get ()->str;
+}
+
+const NMUuid *
+nm_utils_boot_id_bin (void)
+{
+	return &_boot_id_get ()->bin;
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_utils_arp_type_detect_from_hwaddrlen:
+ * @hwaddr_len: the length of the hardware address in bytes.
+ *
+ * Detects the arp-type based on the length of the MAC address.
+ * On success, this returns a (positive) value in uint16_t range,
+ * like ARPHRD_ETHER or ARPHRD_INFINIBAND.
+ *
+ * On failure, returns a negative error code.
+ *
+ * Returns: the arp-type or negative value on error. */
+int
+nm_utils_arp_type_detect_from_hwaddrlen (gsize hwaddr_len)
+{
+	switch (hwaddr_len) {
+	case ETH_ALEN:
+		return ARPHRD_ETHER;
+	case INFINIBAND_ALEN:
+		return ARPHRD_INFINIBAND;
+	default:
+		/* Note: if you ever support anything but ethernet and infiniband,
+		 * make sure to look at all callers. They assert that it's one of
+		 * these two. */
+		return -EINVAL;
+	}
+}
+
+gboolean
+nm_utils_arp_type_validate_hwaddr (int arp_type,
+                                   const guint8 *hwaddr,
+                                   gsize hwaddr_len)
+{
+
+	if (!hwaddr)
+		return FALSE;
+
+	if (arp_type == ARPHRD_ETHER) {
+		G_STATIC_ASSERT (ARPHRD_ETHER >= 0 && ARPHRD_ETHER <= 0xFF);
+		if (hwaddr_len != ETH_ALEN)
+			return FALSE;
+	} else if (arp_type == ARPHRD_INFINIBAND) {
+		G_STATIC_ASSERT (ARPHRD_INFINIBAND >= 0 && ARPHRD_INFINIBAND <= 0xFF);
+		if (hwaddr_len != INFINIBAND_ALEN)
+			return FALSE;
+	} else
+		return FALSE;
+
+	nm_assert (arp_type == nm_utils_arp_type_detect_from_hwaddrlen (hwaddr_len));
+	return TRUE;
+}
+
+gboolean
+nm_utils_arp_type_get_hwaddr_relevant_part (int arp_type,
+                                            const guint8 **hwaddr,
+                                            gsize *hwaddr_len)
+{
+	g_return_val_if_fail (   hwaddr
+	                      && hwaddr_len
+	                      && nm_utils_arp_type_validate_hwaddr (arp_type, *hwaddr, *hwaddr_len),
+	                      FALSE);
+
+	/* for infiniband, we only consider the last 8 bytes. */
+	if (arp_type == ARPHRD_INFINIBAND) {
+		*hwaddr += (INFINIBAND_ALEN - 8);
+		*hwaddr_len = 8;
+	}
+
+	return TRUE;
 }
 
 /*****************************************************************************/
@@ -2920,9 +3324,8 @@ nm_utils_stable_id_random (void)
 char *
 nm_utils_stable_id_generated_complete (const char *stable_id_generated)
 {
-	guint8 buf[20];
-	GChecksum *sum;
-	gsize buf_size;
+	nm_auto_free_checksum GChecksum *sum = NULL;
+	guint8 buf[NM_UTILS_CHECKSUM_LENGTH_SHA1];
 	char *base64;
 
 	/* for NM_UTILS_STABLE_TYPE_GENERATED we genererate a possibly long string
@@ -2933,15 +3336,8 @@ nm_utils_stable_id_generated_complete (const char *stable_id_generated)
 	g_return_val_if_fail (stable_id_generated, NULL);
 
 	sum = g_checksum_new (G_CHECKSUM_SHA1);
-	nm_assert (sum);
-
 	g_checksum_update (sum, (guchar *) stable_id_generated, strlen (stable_id_generated));
-
-	buf_size = sizeof (buf);
-	g_checksum_get_digest (sum, buf, &buf_size);
-	nm_assert (buf_size == sizeof (buf));
-
-	g_checksum_free (sum);
+	nm_utils_checksum_get_digest (sum, buf);
 
 	/* we don't care to use the sha1 sum in common hex representation.
 	 * Use instead base64, it's 27 chars (stripping the padding) vs.
@@ -2967,6 +3363,7 @@ _stable_id_append (GString *str,
 NMUtilsStableType
 nm_utils_stable_id_parse (const char *stable_id,
                           const char *deviceid,
+                          const char *hwaddr,
                           const char *bootid,
                           const char *uuid,
                           char **out_generated)
@@ -3042,9 +3439,11 @@ nm_utils_stable_id_parse (const char *stable_id,
 		if (CHECK_PREFIX ("${CONNECTION}"))
 			_stable_id_append (str, uuid);
 		else if (CHECK_PREFIX ("${BOOT}"))
-			_stable_id_append (str, bootid ?: nm_utils_get_boot_id ());
+			_stable_id_append (str, bootid);
 		else if (CHECK_PREFIX ("${DEVICE}"))
 			_stable_id_append (str, deviceid);
+		else if (CHECK_PREFIX ("${MAC}"))
+			_stable_id_append (str, hwaddr);
 		else if (g_str_has_prefix (&stable_id[i], "${RANDOM}")) {
 			/* RANDOM makes not so much sense for cloned-mac-address
 			 * as the result is simmilar to specyifing "cloned-mac-address=random".
@@ -3121,28 +3520,20 @@ _set_stable_privacy (NMUtilsStableType stable_type,
                      const char *ifname,
                      const char *network_id,
                      guint32 dad_counter,
-                     const guint8 *secret_key,
-                     gsize key_len,
+                     const guint8 *host_id,
+                     gsize host_id_len,
                      GError **error)
 {
-	GChecksum *sum;
-	guint8 digest[32];
+	nm_auto_free_checksum GChecksum *sum = NULL;
+	guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
 	guint32 tmp[2];
-	gsize len = sizeof (digest);
 
-	nm_assert (key_len);
+	nm_assert (host_id_len);
 	nm_assert (network_id);
 
-	/* Documentation suggests that this can fail.
-	 * Maybe in case of a missing algorithm in crypto library? */
 	sum = g_checksum_new (G_CHECKSUM_SHA256);
-	if (!sum) {
-		g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
-		                     "Can't create a SHA256 hash");
-		return FALSE;
-	}
 
-	key_len = MIN (key_len, G_MAXUINT32);
+	host_id_len = MIN (host_id_len, G_MAXUINT32);
 
 	if (stable_type != NM_UTILS_STABLE_TYPE_UUID) {
 		guint8 stable_type_uint8;
@@ -3155,7 +3546,7 @@ _set_stable_privacy (NMUtilsStableType stable_type,
 		 *
 		 * That is no real problem and it is still impossible to
 		 * force a collision here, because of how the remaining
-		 * fields are hashed. That is, as we also hash @key_len
+		 * fields are hashed. That is, as we also hash @host_id_len
 		 * and the terminating '\0' of @network_id, it is unambigiously
 		 * possible to revert the process and deduce the @stable_type.
 		 */
@@ -3166,27 +3557,20 @@ _set_stable_privacy (NMUtilsStableType stable_type,
 	g_checksum_update (sum, (const guchar *) ifname, strlen (ifname) + 1);
 	g_checksum_update (sum, (const guchar *) network_id, strlen (network_id) + 1);
 	tmp[0] = htonl (dad_counter);
-	tmp[1] = htonl (key_len);
+	tmp[1] = htonl (host_id_len);
 	g_checksum_update (sum, (const guchar *) tmp, sizeof (tmp));
-	g_checksum_update (sum, (const guchar *) secret_key, key_len);
-
-	g_checksum_get_digest (sum, digest, &len);
-
-	nm_assert (len == sizeof (digest));
+	g_checksum_update (sum, (const guchar *) host_id, host_id_len);
+	nm_utils_checksum_get_digest (sum, digest);
 
 	while (_is_reserved_ipv6_iid (digest)) {
 		g_checksum_reset (sum);
 		tmp[0] = htonl (++dad_counter);
-		g_checksum_update (sum, digest, len);
+		g_checksum_update (sum, digest, sizeof (digest));
 		g_checksum_update (sum, (const guchar *) &tmp[0], sizeof (tmp[0]));
-		g_checksum_get_digest (sum, digest, &len);
-		nm_assert (len == sizeof (digest));
+		nm_utils_checksum_get_digest (sum, digest);
 	}
 
-	g_checksum_free (sum);
-
 	memcpy (addr->s6_addr + 8, &digest[0], 8);
-
 	return TRUE;
 }
 
@@ -3196,11 +3580,11 @@ nm_utils_ipv6_addr_set_stable_privacy_impl (NMUtilsStableType stable_type,
                                             const char *ifname,
                                             const char *network_id,
                                             guint32 dad_counter,
-                                            guint8 *secret_key,
-                                            gsize key_len,
+                                            guint8 *host_id,
+                                            gsize host_id_len,
                                             GError **error)
 {
-	return _set_stable_privacy (stable_type, addr, ifname, network_id, dad_counter, secret_key, key_len, error);
+	return _set_stable_privacy (stable_type, addr, ifname, network_id, dad_counter, host_id, host_id_len, error);
 }
 
 #define RFC7217_IDGEN_RETRIES 3
@@ -3220,8 +3604,8 @@ nm_utils_ipv6_addr_set_stable_privacy (NMUtilsStableType stable_type,
                                        guint32 dad_counter,
                                        GError **error)
 {
-	const guint8 *secret_key;
-	gsize key_len;
+	const guint8 *host_id;
+	gsize host_id_len;
 
 	g_return_val_if_fail (network_id, FALSE);
 
@@ -3231,10 +3615,10 @@ nm_utils_ipv6_addr_set_stable_privacy (NMUtilsStableType stable_type,
 		return FALSE;
 	}
 
-	nm_utils_secret_key_get (&secret_key, &key_len);
+	nm_utils_host_id_get (&host_id, &host_id_len);
 
 	return _set_stable_privacy (stable_type, addr, ifname, network_id, dad_counter,
-	                            secret_key, key_len, error);
+	                            host_id, host_id_len, error);
 }
 
 /*****************************************************************************/
@@ -3306,42 +3690,36 @@ nm_utils_hw_addr_gen_random_eth (const char *current_mac_address,
 static char *
 _hw_addr_gen_stable_eth (NMUtilsStableType stable_type,
                          const char *stable_id,
-                         const guint8 *secret_key,
-                         gsize key_len,
+                         const guint8 *host_id,
+                         gsize host_id_len,
                          const char *ifname,
                          const char *current_mac_address,
                          const char *generate_mac_address_mask)
 {
-	GChecksum *sum;
+	nm_auto_free_checksum GChecksum *sum = NULL;
 	guint32 tmp;
-	guint8 digest[32];
-	gsize len = sizeof (digest);
+	guint8 digest[NM_UTILS_CHECKSUM_LENGTH_SHA256];
 	struct ether_addr bin_addr;
 	guint8 stable_type_uint8;
 
 	nm_assert (stable_id);
-	nm_assert (secret_key);
+	nm_assert (host_id);
 
 	sum = g_checksum_new (G_CHECKSUM_SHA256);
-	if (!sum)
-		return NULL;
 
-	key_len = MIN (key_len, G_MAXUINT32);
+	host_id_len = MIN (host_id_len, G_MAXUINT32);
 
 	nm_assert (stable_type < (NMUtilsStableType) 255);
 	stable_type_uint8 = stable_type;
 	g_checksum_update (sum, (const guchar *) &stable_type_uint8, sizeof (stable_type_uint8));
 
-	tmp = htonl ((guint32) key_len);
+	tmp = htonl ((guint32) host_id_len);
 	g_checksum_update (sum, (const guchar *) &tmp, sizeof (tmp));
-	g_checksum_update (sum, (const guchar *) secret_key, key_len);
+	g_checksum_update (sum, (const guchar *) host_id, host_id_len);
 	g_checksum_update (sum, (const guchar *) (ifname ?: ""), ifname ? (strlen (ifname) + 1) : 1);
 	g_checksum_update (sum, (const guchar *) stable_id, strlen (stable_id) + 1);
 
-	g_checksum_get_digest (sum, digest, &len);
-	g_checksum_free (sum);
-
-	g_return_val_if_fail (len == 32, NULL);
+	nm_utils_checksum_get_digest (sum, digest);
 
 	memcpy (&bin_addr, digest, ETH_ALEN);
 	_hw_addr_eth_complete (&bin_addr, current_mac_address, generate_mac_address_mask);
@@ -3351,13 +3729,13 @@ _hw_addr_gen_stable_eth (NMUtilsStableType stable_type,
 char *
 nm_utils_hw_addr_gen_stable_eth_impl (NMUtilsStableType stable_type,
                                       const char *stable_id,
-                                      const guint8 *secret_key,
-                                      gsize key_len,
+                                      const guint8 *host_id,
+                                      gsize host_id_len,
                                       const char *ifname,
                                       const char *current_mac_address,
                                       const char *generate_mac_address_mask)
 {
-	return _hw_addr_gen_stable_eth (stable_type, stable_id, secret_key, key_len, ifname, current_mac_address, generate_mac_address_mask);
+	return _hw_addr_gen_stable_eth (stable_type, stable_id, host_id, host_id_len, ifname, current_mac_address, generate_mac_address_mask);
 }
 
 char *
@@ -3367,17 +3745,17 @@ nm_utils_hw_addr_gen_stable_eth (NMUtilsStableType stable_type,
                                  const char *current_mac_address,
                                  const char *generate_mac_address_mask)
 {
-	const guint8 *secret_key;
-	gsize key_len;
+	const guint8 *host_id;
+	gsize host_id_len;
 
 	g_return_val_if_fail (stable_id, NULL);
 
-	nm_utils_secret_key_get (&secret_key, &key_len);
+	nm_utils_host_id_get (&host_id, &host_id_len);
 
 	return _hw_addr_gen_stable_eth (stable_type,
 	                                stable_id,
-	                                secret_key,
-	                                key_len,
+	                                host_id,
+	                                host_id_len,
 	                                ifname,
 	                                current_mac_address,
 	                                generate_mac_address_mask);
@@ -3385,6 +3763,118 @@ nm_utils_hw_addr_gen_stable_eth (NMUtilsStableType stable_type,
 
 /*****************************************************************************/
 
+GBytes *
+nm_utils_dhcp_client_id_mac (int arp_type,
+                             const guint8 *hwaddr,
+                             gsize hwaddr_len)
+{
+	guint8 *client_id_buf;
+	const guint8 hwaddr_type = arp_type;
+
+	if (!nm_utils_arp_type_get_hwaddr_relevant_part (arp_type, &hwaddr, &hwaddr_len))
+		g_return_val_if_reached (NULL);
+
+	client_id_buf = g_malloc (hwaddr_len + 1);
+	client_id_buf[0] = hwaddr_type;
+	memcpy (&client_id_buf[1], hwaddr, hwaddr_len);
+	return g_bytes_new_take (client_id_buf, hwaddr_len + 1);
+}
+
+/**
+ * nm_utils_dhcp_client_id_systemd_node_specific_full:
+ * @legacy_unstable_byteorder: historically, the code would generate a iaid
+ *   dependent on host endianness. This is undesirable, if backward compatibility
+ *   are not a concern, generate stable endianness.
+ * @interface_id: a binary identifer that is hashed into the DUID.
+ *   Comonly this is the interface-name, but it may be the MAC address.
+ * @interface_id_len: the length of @interface_id.
+ * @machine_id: the binary identifier for the machine. It is hashed
+ *   into the DUID. It commonly is /etc/machine-id (parsed in binary as NMUuid).
+ * @machine_id_len: the length of the @machine_id.
+ *
+ * Systemd's sd_dhcp_client generates a default client ID (type 255, node-specific,
+ * RFC 4361) if no explicit client-id is set. This function duplicates that
+ * implementation and exposes it as (internal) API.
+ *
+ * Returns: a %GBytes of generated client-id. This function cannot fail.
+ */
+GBytes *
+nm_utils_dhcp_client_id_systemd_node_specific_full (gboolean legacy_unstable_byteorder,
+                                                    const guint8 *interface_id,
+                                                    gsize interface_id_len,
+                                                    const guint8 *machine_id,
+                                                    gsize machine_id_len)
+{
+	const guint8 HASH_KEY[16] = { 0x80, 0x11, 0x8c, 0xc2, 0xfe, 0x4a, 0x03, 0xee, 0x3e, 0xd6, 0x0c, 0x6f, 0x36, 0x39, 0x14, 0x09 };
+	const guint16 DUID_TYPE_EN = 2;
+	const guint32 SYSTEMD_PEN = 43793;
+	struct _nm_packed {
+		guint8 type;
+		guint32 iaid;
+		struct _nm_packed {
+			guint16 type;
+			union {
+				struct _nm_packed {
+						/* DUID_TYPE_EN */
+						guint32 pen;
+						uint8_t id[8];
+				} en;
+			};
+		} duid;
+	} *client_id;
+	guint64 u64;
+	guint32 u32;
+
+	g_return_val_if_fail (interface_id, NULL);
+	g_return_val_if_fail (interface_id_len > 0, NULL);
+	g_return_val_if_fail (machine_id, NULL);
+	g_return_val_if_fail (machine_id_len > 0, NULL);
+
+	client_id = g_malloc (sizeof (*client_id));
+
+	client_id->type = 255;
+
+	u64 = c_siphash_hash (HASH_KEY, interface_id, interface_id_len);
+	u32 = (u64 & 0xffffffffu) ^ (u64 >> 32);
+	if (legacy_unstable_byteorder) {
+		/* original systemd code dhcp_identifier_set_iaid() generates the iaid
+		 * in native endianness. Do that too, to preserve compatibility
+		 * (https://github.com/systemd/systemd/pull/10614). */
+		u32 = bswap_32 (u32);
+	} else {
+		/* generate fixed byteorder, in a way that on little endian systems
+		 * the values agree. Meaning: legacy behavior is identical to this
+		 * on little endian. */
+		u32 = be32toh (u32);
+	}
+	unaligned_write_ne32 (&client_id->iaid, u32);
+
+	unaligned_write_be16 (&client_id->duid.type, DUID_TYPE_EN);
+
+	unaligned_write_be32 (&client_id->duid.en.pen, SYSTEMD_PEN);
+
+	u64 = htole64 (c_siphash_hash (HASH_KEY, machine_id, machine_id_len));
+	memcpy(client_id->duid.en.id, &u64, sizeof (client_id->duid.en.id));
+
+	G_STATIC_ASSERT_EXPR (sizeof (*client_id) == 19);
+	return g_bytes_new_take (client_id, 19);
+}
+
+GBytes *
+nm_utils_dhcp_client_id_systemd_node_specific (gboolean legacy_unstable_byteorder,
+                                               const char *ifname)
+{
+	g_return_val_if_fail (ifname && ifname[0], NULL);
+
+	return nm_utils_dhcp_client_id_systemd_node_specific_full (legacy_unstable_byteorder,
+	                                                           (const guint8 *) ifname,
+	                                                           strlen (ifname),
+	                                                           (const guint8 *) nm_utils_machine_id_bin (),
+	                                                           sizeof (NMUuid));
+}
+
+/*****************************************************************************/
+
 /**
  * nm_utils_setpgid:
  * @unused: unused