summary refs log tree commit diff
path: root/shared/nm-glib-aux
diff options
context:
space:
mode:
Diffstat (limited to 'shared/nm-glib-aux')
-rw-r--r--shared/nm-glib-aux/nm-dbus-aux.c124
-rw-r--r--shared/nm-glib-aux/nm-dbus-aux.h37
-rw-r--r--shared/nm-glib-aux/nm-enum-utils.c23
-rw-r--r--shared/nm-glib-aux/nm-errno.c3
-rw-r--r--shared/nm-glib-aux/nm-glib.h55
-rw-r--r--shared/nm-glib-aux/nm-hash-utils.c112
-rw-r--r--shared/nm-glib-aux/nm-hash-utils.h23
-rw-r--r--shared/nm-glib-aux/nm-io-utils.c76
-rw-r--r--shared/nm-glib-aux/nm-io-utils.h4
-rw-r--r--shared/nm-glib-aux/nm-jansson.h2
-rw-r--r--shared/nm-glib-aux/nm-logging-fwd.h8
-rw-r--r--shared/nm-glib-aux/nm-macros-internal.h204
-rw-r--r--shared/nm-glib-aux/nm-ref-string.h12
-rw-r--r--shared/nm-glib-aux/nm-secret-utils.c28
-rw-r--r--shared/nm-glib-aux/nm-secret-utils.h130
-rw-r--r--shared/nm-glib-aux/nm-shared-utils.c856
-rw-r--r--shared/nm-glib-aux/nm-shared-utils.h495
-rw-r--r--shared/nm-glib-aux/nm-str-buf.h332
-rw-r--r--shared/nm-glib-aux/nm-time-utils.c90
-rw-r--r--shared/nm-glib-aux/nm-time-utils.h36
-rw-r--r--shared/nm-glib-aux/tests/meson.build23
-rw-r--r--shared/nm-glib-aux/tests/test-shared-general.c797
22 files changed, 2985 insertions, 485 deletions
diff --git a/shared/nm-glib-aux/nm-dbus-aux.c b/shared/nm-glib-aux/nm-dbus-aux.c
index 86061aef..54e54ca7 100644
--- a/shared/nm-glib-aux/nm-dbus-aux.c
+++ b/shared/nm-glib-aux/nm-dbus-aux.c
@@ -56,7 +56,7 @@ nm_dbus_connection_call_get_name_owner (GDBusConnection *dbus_connection,
 /*****************************************************************************/
 
 static void
-_nm_dbus_connection_call_get_all_cb (GObject *source,
+_nm_dbus_connection_call_default_cb (GObject *source,
                                      GAsyncResult *res,
                                      gpointer user_data)
 {
@@ -96,100 +96,36 @@ nm_dbus_connection_call_get_all (GDBusConnection *dbus_connection,
 	                        G_DBUS_CALL_FLAGS_NONE,
 	                        timeout_msec,
 	                        cancellable,
-	                        _nm_dbus_connection_call_get_all_cb,
+	                        _nm_dbus_connection_call_default_cb,
 	                        nm_utils_user_data_pack (user_data, callback));
 }
 
-/*****************************************************************************/
-
-typedef struct {
-	NMDBusConnectionSignalObjectMangerCb callback;
-	gpointer user_data;
-	GDestroyNotify user_data_free_func;
-} SubscribeObjectManagerData;
-
-static void
-_subscribe_object_manager_cb (GDBusConnection *connection,
-                              const char *sender_name,
-                              const char *arg_object_path,
-                              const char *interface_name,
-                              const char *signal_name,
-                              GVariant *parameters,
-                              gpointer user_data)
-{
-	const SubscribeObjectManagerData *d = user_data;
-
-	nm_assert (nm_streq0 (interface_name, DBUS_INTERFACE_OBJECT_MANAGER));
-
-	if (nm_streq (signal_name, "InterfacesAdded")) {
-		gs_unref_variant GVariant *interfaces_and_properties = NULL;
-		const char *object_path;
-
-		if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(oa{sa{sv}})")))
-			return;
-
-		g_variant_get (parameters,
-		               "(&o@a{sa{sv}})",
-		               &object_path,
-		               &interfaces_and_properties);
-
-		d->callback (object_path, interfaces_and_properties, NULL, d->user_data);
-		return;
-	}
-
-	if (nm_streq (signal_name, "InterfacesRemoved")) {
-		gs_free const char **interfaces = NULL;
-		const char *object_path;
-
-		if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(oas)")))
-			return;
-
-		g_variant_get (parameters,
-		               "(&o^a&s)",
-		               &object_path,
-		               &interfaces);
-
-		d->callback (object_path, NULL, interfaces, d->user_data);
-		return;
-	}
-}
-
-static void
-_subscribe_object_manager_data_free (gpointer ptr)
-{
-	SubscribeObjectManagerData *d = ptr;
-
-	if (d->user_data_free_func)
-		d->user_data_free_func (d->user_data);
-	nm_g_slice_free (d);
-}
-
-guint
-nm_dbus_connection_signal_subscribe_object_manager (GDBusConnection *dbus_connection,
-                                                    const char *service_name,
-                                                    const char *object_path,
-                                                    NMDBusConnectionSignalObjectMangerCb callback,
-                                                    gpointer user_data,
-                                                    GDestroyNotify user_data_free_func)
+void nm_dbus_connection_call_set (GDBusConnection *dbus_connection,
+                                  const char *bus_name,
+                                  const char *object_path,
+                                  const char *interface_name,
+                                  const char *property_name,
+                                  GVariant *value,
+                                  int timeout_msec,
+                                  GCancellable *cancellable,
+                                  NMDBusConnectionCallDefaultCb callback,
+                                  gpointer user_data)
 {
-	SubscribeObjectManagerData *d;
-
-	g_return_val_if_fail (callback, 0);
-
-	d = g_slice_new (SubscribeObjectManagerData);
-	*d = (SubscribeObjectManagerData) {
-		.callback            = callback,
-		.user_data           = user_data,
-		.user_data_free_func = user_data_free_func,
-	};
-
-	return nm_dbus_connection_signal_subscribe_object_manager_plain (dbus_connection,
-	                                                                 service_name,
-	                                                                 object_path,
-	                                                                 NULL,
-	                                                                 _subscribe_object_manager_cb,
-	                                                                 d,
-	                                                                 _subscribe_object_manager_data_free);
+	g_dbus_connection_call (dbus_connection,
+	                        bus_name,
+	                        object_path,
+	                        DBUS_INTERFACE_PROPERTIES,
+	                        "Set",
+	                        g_variant_new ("(ssv)",
+	                                       interface_name,
+	                                       property_name,
+	                                       value),
+	                        G_VARIANT_TYPE ("()"),
+	                        G_DBUS_CALL_FLAGS_NONE,
+	                        timeout_msec,
+	                        cancellable,
+	                        callback ? _nm_dbus_connection_call_default_cb : NULL,
+	                        callback ? nm_utils_user_data_pack (user_data, callback) : NULL);
 }
 
 /*****************************************************************************/
@@ -269,10 +205,9 @@ _call_finish_cb (GObject *source,
 		return;
 	}
 
-	if (!return_void) {
-		nm_assert (!g_variant_is_of_type (ret, G_VARIANT_TYPE ("()")));
+	if (!return_void)
 		g_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify) g_variant_unref);
-	} else {
+	else {
 		nm_assert (g_variant_is_of_type (ret, G_VARIANT_TYPE ("()")));
 		g_task_return_boolean (task, TRUE);
 	}
@@ -317,7 +252,6 @@ nm_dbus_connection_call_finish_void_strip_dbus_error_cb (GObject *source,
  *
  * - user_data must be a GTask, whose reference will be consumed by the
  *   callback.
- * - the return GVariant must not be an empty tuple "()".
  * - the GTask is returned either with error or with a pointer containing the GVariant.
  */
 void
diff --git a/shared/nm-glib-aux/nm-dbus-aux.h b/shared/nm-glib-aux/nm-dbus-aux.h
index 840e23c2..fcf394d1 100644
--- a/shared/nm-glib-aux/nm-dbus-aux.h
+++ b/shared/nm-glib-aux/nm-dbus-aux.h
@@ -128,16 +128,27 @@ void nm_dbus_connection_call_get_all (GDBusConnection *dbus_connection,
                                       NMDBusConnectionCallDefaultCb callback,
                                       gpointer user_data);
 
+void nm_dbus_connection_call_set (GDBusConnection *dbus_connection,
+                                  const char *bus_name,
+                                  const char *object_path,
+                                  const char *interface_name,
+                                  const char *property_name,
+                                  GVariant *value,
+                                  int timeout_msec,
+                                  GCancellable *cancellable,
+                                  NMDBusConnectionCallDefaultCb callback,
+                                  gpointer user_data);
+
 /*****************************************************************************/
 
 static inline guint
-nm_dbus_connection_signal_subscribe_object_manager_plain (GDBusConnection *dbus_connection,
-                                                          const char *service_name,
-                                                          const char *object_path,
-                                                          const char *signal_name,
-                                                          GDBusSignalCallback callback,
-                                                          gpointer user_data,
-                                                          GDestroyNotify user_data_free_func)
+nm_dbus_connection_signal_subscribe_object_manager (GDBusConnection *dbus_connection,
+                                                    const char *service_name,
+                                                    const char *object_path,
+                                                    const char *signal_name,
+                                                    GDBusSignalCallback callback,
+                                                    gpointer user_data,
+                                                    GDestroyNotify user_data_free_func)
 {
 	return g_dbus_connection_signal_subscribe (dbus_connection,
 	                                           service_name,
@@ -151,18 +162,6 @@ nm_dbus_connection_signal_subscribe_object_manager_plain (GDBusConnection *dbus_
 	                                           user_data_free_func);
 }
 
-typedef void (*NMDBusConnectionSignalObjectMangerCb) (const char *object_path,
-                                                      GVariant *added_interfaces_and_properties,
-                                                      const char *const*removed_interfaces,
-                                                      gpointer user_data);
-
-guint nm_dbus_connection_signal_subscribe_object_manager (GDBusConnection *dbus_connection,
-                                                          const char *service_name,
-                                                          const char *object_path,
-                                                          NMDBusConnectionSignalObjectMangerCb callback,
-                                                          gpointer user_data,
-                                                          GDestroyNotify user_data_free_func);
-
 void nm_dbus_connection_call_get_managed_objects (GDBusConnection *dbus_connection,
                                                   const char *bus_name,
                                                   const char *object_path,
diff --git a/shared/nm-glib-aux/nm-enum-utils.c b/shared/nm-glib-aux/nm-enum-utils.c
index e105a4f5..854eda6e 100644
--- a/shared/nm-glib-aux/nm-enum-utils.c
+++ b/shared/nm-glib-aux/nm-enum-utils.c
@@ -6,6 +6,7 @@
 #include "nm-default.h"
 
 #include "nm-enum-utils.h"
+#include "nm-str-buf.h"
 
 /*****************************************************************************/
 
@@ -142,12 +143,14 @@ _nm_utils_enum_to_str_full (GType type,
 		else
 			return g_strdup (enum_value->value_nick);
 	} else if (G_IS_FLAGS_CLASS (klass)) {
-		GFlagsValue *flags_value;
-		GString *str = g_string_new ("");
 		unsigned uvalue = (unsigned) value;
+		GFlagsValue *flags_value;
+		NMStrBuf strbuf;
 
 		flags_separator = flags_separator ?: " ";
 
+		nm_str_buf_init (&strbuf, 16, FALSE);
+
 		for ( ; value_infos && value_infos->nick; value_infos++) {
 
 			nm_assert (_enum_is_valid_flags_nick (value_infos->nick));
@@ -160,9 +163,9 @@ _nm_utils_enum_to_str_full (GType type,
 					continue;
 			}
 
-			if (str->len)
-				g_string_append (str, flags_separator);
-			g_string_append (str, value_infos->nick);
+			if (strbuf.len)
+				nm_str_buf_append (&strbuf, flags_separator);
+			nm_str_buf_append (&strbuf, value_infos->nick);
 			uvalue &= ~((unsigned) value_infos->value);
 			if (uvalue == 0) {
 				/* we printed all flags. Done. */
@@ -172,20 +175,20 @@ _nm_utils_enum_to_str_full (GType type,
 
 		do {
 			flags_value = g_flags_get_first_value (G_FLAGS_CLASS (klass), uvalue);
-			if (str->len)
-				g_string_append (str, flags_separator);
+			if (strbuf.len)
+				nm_str_buf_append (&strbuf, flags_separator);
 			if (   !flags_value
 			    || !_enum_is_valid_flags_nick (flags_value->value_nick)) {
 				if (uvalue)
-					g_string_append_printf (str, "0x%x", uvalue);
+					nm_str_buf_append_printf (&strbuf, "0x%x", uvalue);
 				break;
 			}
-			g_string_append (str, flags_value->value_nick);
+			nm_str_buf_append (&strbuf, flags_value->value_nick);
 			uvalue &= ~flags_value->value;
 		} while (uvalue);
 
 flags_done:
-		return g_string_free (str, FALSE);
+		return nm_str_buf_finalize (&strbuf, NULL);
 	}
 
 	g_return_val_if_reached (NULL);
diff --git a/shared/nm-glib-aux/nm-errno.c b/shared/nm-glib-aux/nm-errno.c
index 9133d87d..e709f9e6 100644
--- a/shared/nm-glib-aux/nm-errno.c
+++ b/shared/nm-glib-aux/nm-errno.c
@@ -11,7 +11,8 @@
 
 /*****************************************************************************/
 
-NM_UTILS_LOOKUP_STR_DEFINE_STATIC (_geterror,
+static
+NM_UTILS_LOOKUP_STR_DEFINE (_geterror,
 #if 0
 	enum _NMErrno,
 #else
diff --git a/shared/nm-glib-aux/nm-glib.h b/shared/nm-glib-aux/nm-glib.h
index 26d0eacd..4ecba9ff 100644
--- a/shared/nm-glib-aux/nm-glib.h
+++ b/shared/nm-glib-aux/nm-glib.h
@@ -1,4 +1,4 @@
-// SPDX-License-Identifier: GPL-2.0+
+// SPDX-License-Identifier: LGPL-2.1+
 /*
  * Copyright (C) 2008 - 2018 Red Hat, Inc.
  */
@@ -193,22 +193,6 @@ __g_test_add_data_func_full (const char     *testpath,
 
 /*****************************************************************************/
 
-#if !GLIB_CHECK_VERSION (2, 34, 0)
-#define G_DEFINE_QUARK(QN, q_n)               \
-GQuark                                        \
-q_n##_quark (void)                            \
-{                                             \
-	static GQuark q;                          \
-                                              \
-	if G_UNLIKELY (q == 0)                    \
-		q = g_quark_from_static_string (#QN); \
-                                              \
-	return q;                                 \
-}
-#endif
-
-/*****************************************************************************/
-
 static inline gboolean
 nm_g_hash_table_replace (GHashTable *hash, gpointer key, gpointer value)
 {
@@ -601,4 +585,41 @@ _g_atomic_pointer_compare_and_exchange (volatile void *atomic,
 
 /*****************************************************************************/
 
+#if !GLIB_CHECK_VERSION (2, 58, 0)
+static inline gboolean
+g_hash_table_steal_extended (GHashTable    *hash_table,
+                             gconstpointer  lookup_key,
+                             gpointer      *stolen_key,
+                             gpointer      *stolen_value)
+{
+	g_assert (stolen_key);
+	g_assert (stolen_value);
+
+	if (g_hash_table_lookup_extended (hash_table, lookup_key, stolen_key, stolen_value)) {
+		g_hash_table_steal (hash_table, lookup_key);
+		return TRUE;
+	}
+	*stolen_key = NULL;
+	*stolen_value = NULL;
+	return FALSE;
+}
+#else
+#define g_hash_table_steal_extended(hash_table, lookup_key, stolen_key, stolen_value) \
+	({ \
+		gpointer *_stolen_key = (stolen_key); \
+		gpointer *_stolen_value = (stolen_value); \
+		\
+		/* we cannot allow NULL arguments, because then we would leak the values in
+		 * the compat implementation. */ \
+		g_assert (_stolen_key); \
+		g_assert (_stolen_value); \
+		\
+		G_GNUC_BEGIN_IGNORE_DEPRECATIONS \
+		g_hash_table_steal_extended (hash_table, lookup_key, _stolen_key, _stolen_value); \
+		G_GNUC_END_IGNORE_DEPRECATIONS \
+	})
+#endif
+
+/*****************************************************************************/
+
 #endif  /* __NM_GLIB_H__ */
diff --git a/shared/nm-glib-aux/nm-hash-utils.c b/shared/nm-glib-aux/nm-hash-utils.c
index cd51bbaf..0a701d06 100644
--- a/shared/nm-glib-aux/nm-hash-utils.c
+++ b/shared/nm-glib-aux/nm-hash-utils.c
@@ -24,7 +24,6 @@ static const guint8 *volatile global_seed = NULL;
 static const guint8 *
 _get_hash_key_init (void)
 {
-	static gsize g_lock;
 	/* the returned hash is aligned to guin64, hence, it is safe
 	 * to use it as guint* or guint64* pointer. */
 	static union {
@@ -34,50 +33,49 @@ _get_hash_key_init (void)
 		guint64 _align_as_uint64;
 	} g_arr;
 	const guint8 *g;
-	union {
-		guint8 v8[HASH_KEY_SIZE];
-		guint vuint;
-	} t_arr;
 
 again:
 	g = g_atomic_pointer_get (&global_seed);
-	if (G_LIKELY (g != NULL)) {
-		nm_assert (g == g_arr.v8);
-		return g;
-	}
-
-	{
-		CSipHash siph_state;
+	if (!G_UNLIKELY (g)) {
+		static gsize g_lock;
 		uint64_t h;
-
-		/* initialize a random key in t_arr. */
+		union {
+			guint vuint;
+			guint8 v8[HASH_KEY_SIZE];
+			guint8 _extra_entropy[3 * HASH_KEY_SIZE];
+		} t_arr;
 
 		nm_utils_random_bytes (&t_arr, sizeof (t_arr));
 
-		/* use siphash() of the key-size, to mangle the first guint. Otherwise,
-		 * the first guint has only the entropy that nm_utils_random_bytes()
-		 * generated for the first 4 bytes and relies on a good random generator.
+		/* We only initialize one random hash key. So we can spend some effort
+		 * of getting this right. For one, we collect more random bytes than
+		 * necessary.
 		 *
-		 * The first int is especially interesting for nm_hash_static() below, and we
-		 * want to have it all the entropy of t_arr. */
-		c_siphash_init (&siph_state, t_arr.v8);
-		c_siphash_append (&siph_state, (const guint8 *) &t_arr, sizeof (t_arr));
-		h = c_siphash_finalize (&siph_state);
-		if (sizeof (guint) < sizeof (h))
-			t_arr.vuint = t_arr.vuint ^ ((guint) (h & 0xFFFFFFFFu)) ^ ((guint) (h >> 32));
+		 * Then, the first guint of the seed should have all the entropy that we could
+		 * obtain in sizeof(t_arr). For that, siphash(t_arr) and xor the first guint
+		 * with hash.
+		 * The first guint is especially interesting for nm_hash_static() below that
+		 * doesn't use siphash itself. */
+		h = c_siphash_hash (t_arr.v8,
+		                    (const guint8 *) &t_arr,
+		                    sizeof (t_arr));
+		if (sizeof (h) > sizeof (guint))
+			t_arr.vuint = t_arr.vuint ^ ((guint) (h & G_MAXUINT)) ^ ((guint) (h >> 32));
 		else
-			t_arr.vuint = t_arr.vuint ^ ((guint) (h & 0xFFFFFFFFu));
-	}
+			t_arr.vuint = t_arr.vuint ^ ((guint) (h & G_MAXUINT));
 
-	if (!g_once_init_enter (&g_lock)) {
-		/* lost a race. The random key is already initialized. */
-		goto again;
+		if (!g_once_init_enter (&g_lock)) {
+			/* lost a race. The random key is already initialized. */
+			goto again;
+		}
+
+		memcpy (g_arr.v8, t_arr.v8, HASH_KEY_SIZE);
+		g = g_arr.v8;
+		g_atomic_pointer_set (&global_seed, g);
+		g_once_init_leave (&g_lock, 1);
 	}
 
-	memcpy (g_arr.v8, t_arr.v8, HASH_KEY_SIZE);
-	g = g_arr.v8;
-	g_atomic_pointer_set (&global_seed, g);
-	g_once_init_leave (&g_lock, 1);
+	nm_assert (g == g_arr.v8);
 	return g;
 }
 
@@ -94,18 +92,24 @@ again:
 guint
 nm_hash_static (guint static_seed)
 {
-	/* note that we only xor the static_seed with the key.
-	 * We don't use siphash, which would mix the bits better.
-	 * Note that this doesn't matter, because static_seed is not
-	 * supposed to be a value that you are hashing (for that, use
-	 * full siphash).
-	 * Instead, different callers may set a different static_seed
-	 * so that nm_hash_str(NULL) != nm_hash_ptr(NULL).
+	/* Note that we only xor the static_seed with the first guint of the key.
+	 *
+	 * We don't use siphash, which would mix the bits better with _get_hash_key().
+	 * Note that nm_hash_static() isn't used to hash the static_seed. Instead, it
+	 * is used to get a unique hash value in a static context. That means, every
+	 * caller is responsible to choose a static_seed that is sufficiently
+	 * distinct from all other callers. In other words, static_seed should be a
+	 * unique constant with good entropy.
+	 *
+	 * Note that _get_hash_key_init() already xored the first guint of the
+	 * key with the siphash of the entire static key. That means, even if
+	 * we got bad randomness for the first guint, the first guint is also
+	 * mixed with the randomness of the entire random key.
 	 *
-	 * Also, ensure that we don't return zero.
+	 * Also, ensure that we don't return zero (like for nm_hash_complete()).
 	 */
-	return ((*((const guint *) _get_hash_key ())) ^ static_seed)
-	       ?: static_seed ?: 3679500967u;
+	return    ((*((const guint *) _get_hash_key ())) ^ static_seed)
+	       ?: 3679500967u;
 }
 
 void
@@ -257,3 +261,25 @@ nm_ppdirect_equal (gconstpointer a, gconstpointer b)
 
 	return **s1 == **s2;
 }
+
+/*****************************************************************************/
+
+guint
+nm_pgbytes_hash (gconstpointer p)
+{
+	GBytes *const*ptr = p;
+	gconstpointer arr;
+	gsize len;
+
+	arr = g_bytes_get_data (*ptr, &len);
+	return nm_hash_mem (1470631313u, arr, len);
+}
+
+gboolean
+nm_pgbytes_equal (gconstpointer a, gconstpointer b)
+{
+	GBytes *const*ptr_a = a;
+	GBytes *const*ptr_b = b;
+
+	return g_bytes_equal (*ptr_a, *ptr_b);
+}
diff --git a/shared/nm-glib-aux/nm-hash-utils.h b/shared/nm-glib-aux/nm-hash-utils.h
index 21c5e584..9f2e9766 100644
--- a/shared/nm-glib-aux/nm-hash-utils.h
+++ b/shared/nm-glib-aux/nm-hash-utils.h
@@ -88,15 +88,15 @@ nm_hash_complete (NMHashState *state)
 	/* we don't ever want to return a zero hash.
 	 *
 	 * NMPObject requires that in _idx_obj_part(), and it's just a good idea. */
-	return (((guint) (h >> 32)) ^ ((guint) h)) ?: 1396707757u;
+	return    (((guint) (h >> 32)) ^ ((guint) h))
+	       ?: 1396707757u;
 }
 
 static inline void
 nm_hash_update (NMHashState *state, const void *ptr, gsize n)
 {
 	nm_assert (state);
-	nm_assert (ptr);
-	nm_assert (n > 0);
+	nm_assert (n == 0 || ptr);
 
 	/* Note: the data passed in here might be sensitive data (secrets),
 	 * that we should nm_explicty_zero() afterwards. However, since
@@ -267,6 +267,18 @@ guint nm_str_hash (gconstpointer str);
 		nm_hash_complete (&_h); \
 	})
 
+static inline guint
+nm_hash_mem (guint static_seed, const void *ptr, gsize n)
+{
+	NMHashState h;
+
+	if (n == 0)
+		return nm_hash_static (static_seed);
+	nm_hash_init (&h, static_seed);
+	nm_hash_update (&h, ptr, n);
+	return nm_hash_complete (&h);
+}
+
 /*****************************************************************************/
 
 /* nm_pstr_*() are for hashing keys that are pointers to strings,
@@ -303,6 +315,11 @@ gboolean nm_ppdirect_equal (gconstpointer a, gconstpointer b);
 
 /*****************************************************************************/
 
+guint nm_pgbytes_hash (gconstpointer p);
+gboolean nm_pgbytes_equal (gconstpointer a, gconstpointer b);
+
+/*****************************************************************************/
+
 #define NM_HASH_OBFUSCATE_PTR_FMT "%016" G_GINT64_MODIFIER "x"
 
 /* sometimes we want to log a pointer directly, for providing context/information about
diff --git a/shared/nm-glib-aux/nm-io-utils.c b/shared/nm-glib-aux/nm-io-utils.c
index ec0cd4c8..776c63e1 100644
--- a/shared/nm-glib-aux/nm-io-utils.c
+++ b/shared/nm-glib-aux/nm-io-utils.c
@@ -11,6 +11,7 @@
 #include <sys/stat.h>
 #include <fcntl.h>
 
+#include "nm-str-buf.h"
 #include "nm-shared-utils.h"
 #include "nm-secret-utils.h"
 #include "nm-errno.h"
@@ -51,32 +52,6 @@ _get_contents_error (GError **error, int errsv, int *out_errsv, const char *form
 		_get_contents_error (error, _errsv, out_errsv, __VA_ARGS__); \
 	})
 
-static char *
-_mem_realloc (char *old, gboolean do_bzero_mem, gsize cur_len, gsize new_len)
-{
-	char *new;
-
-	/* re-allocating to zero bytes is an odd case. We don't need it
-	 * and it's not supported. */
-	nm_assert (new_len > 0);
-
-	/* regardless of success/failure, @old will always be freed/consumed. */
-
-	if (do_bzero_mem && cur_len > 0) {
-		new = g_try_malloc (new_len);
-		if (new)
-			memcpy (new, old, NM_MIN (cur_len, new_len));
-		nm_explicit_bzero (old, cur_len);
-		g_free (old);
-	} else {
-		new = g_try_realloc (old, new_len);
-		if (!new)
-			g_free (old);
-	}
-
-	return new;
-}
-
 /**
  * nm_utils_fd_get_contents:
  * @fd: open file descriptor to read. The fd will not be closed,
@@ -161,7 +136,7 @@ nm_utils_fd_get_contents (int fd,
 		str[n_read] = '\0';
 
 		if (n_read < n_stat) {
-			if (!(str = _mem_realloc (str, do_bzero_mem, n_stat + 1, n_read + 1)))
+			if (!(str = nm_secret_mem_try_realloc_take (str, do_bzero_mem, n_stat + 1, n_read + 1)))
 				return _get_contents_error (error, ENOMEM, out_errsv, "failure to reallocate buffer with %zu bytes", n_read + 1);
 		}
 		NM_SET_OUT (length, n_read);
@@ -222,7 +197,7 @@ nm_utils_fd_get_contents (int fd,
 					n_alloc = NM_MIN (n_read + 1, sizeof (buf));
 				}
 
-				if (!(str = _mem_realloc (str, do_bzero_mem, old_n_alloc, n_alloc))) {
+				if (!(str = nm_secret_mem_try_realloc_take (str, do_bzero_mem, old_n_alloc, n_alloc))) {
 					if (do_bzero_mem)
 						nm_explicit_bzero (buf, sizeof (buf));
 					return _get_contents_error (error, ENOMEM, out_errsv, "failure to allocate buffer of %zu bytes", n_alloc);
@@ -241,7 +216,7 @@ nm_utils_fd_get_contents (int fd,
 		else {
 			str[n_have] = '\0';
 			if (n_have + 1 < n_alloc) {
-				if (!(str = _mem_realloc (str, do_bzero_mem, n_alloc, n_have + 1)))
+				if (!(str = nm_secret_mem_try_realloc_take (str, do_bzero_mem, n_alloc, n_have + 1)))
 					return _get_contents_error (error, ENOMEM, out_errsv, "failure to truncate buffer to %zu bytes", n_have + 1);
 			}
 		}
@@ -438,3 +413,46 @@ nm_utils_file_stat (const char *filename, struct stat *out_st)
 		return -NM_ERRNO_NATIVE (errno);
 	return 0;
 }
+
+/**
+ * nm_utils_fd_read:
+ * @fd: the fd to read from.
+ * @out_string: (out): output string where read bytes will be stored.
+ *
+ * Returns: <0 on failure, which is -(errno).
+ *          0 on EOF.
+ *          >0 on success, which is the number of bytes read.  */
+gssize
+nm_utils_fd_read (int fd, NMStrBuf *out_string)
+{
+	gsize buf_available;
+	gssize n_read;
+	int errsv;
+
+	g_return_val_if_fail (fd >= 0, -1);
+	g_return_val_if_fail (out_string, -1);
+
+	/* If the buffer size is 0, we allocate NM_UTILS_GET_NEXT_REALLOC_SIZE_1000 (1000 bytes)
+	 * the first time. Afterwards, the buffer grows exponentially.
+	 *
+	 * Note that with @buf_available, we always would read as much buffer as we actually
+	 * have reserved. */
+	nm_str_buf_maybe_expand (out_string, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, FALSE);
+
+	buf_available = out_string->allocated - out_string->len;
+
+	n_read = read (fd,
+	               &((nm_str_buf_get_str_unsafe (out_string))[out_string->len]),
+	               buf_available);
+	if (n_read < 0) {
+		errsv = errno;
+		return -NM_ERRNO_NATIVE (errsv);
+	}
+
+	if (n_read > 0) {
+		nm_assert ((gsize) n_read <= buf_available);
+		nm_str_buf_set_size (out_string, out_string->len + (gsize) n_read, TRUE, FALSE);
+	}
+
+	return n_read;
+}
diff --git a/shared/nm-glib-aux/nm-io-utils.h b/shared/nm-glib-aux/nm-io-utils.h
index 18dc5f74..31326fc7 100644
--- a/shared/nm-glib-aux/nm-io-utils.h
+++ b/shared/nm-glib-aux/nm-io-utils.h
@@ -47,6 +47,10 @@ gboolean nm_utils_file_set_contents (const char *filename,
                                      int *out_errsv,
                                      GError **error);
 
+struct _NMStrBuf;
+
+gssize nm_utils_fd_read (int fd, struct _NMStrBuf *out_string);
+
 struct stat;
 
 int nm_utils_file_stat (const char *filename, struct stat *out_st);
diff --git a/shared/nm-glib-aux/nm-jansson.h b/shared/nm-glib-aux/nm-jansson.h
index 0a75cff5..7c034222 100644
--- a/shared/nm-glib-aux/nm-jansson.h
+++ b/shared/nm-glib-aux/nm-jansson.h
@@ -1,4 +1,4 @@
-// SPDX-License-Identifier: GPL-2.0+
+// SPDX-License-Identifier: LGPL-2.1+
 /*
  * Copyright (C) 2018 Red Hat, Inc.
  */
diff --git a/shared/nm-glib-aux/nm-logging-fwd.h b/shared/nm-glib-aux/nm-logging-fwd.h
index 47661788..2daa302f 100644
--- a/shared/nm-glib-aux/nm-logging-fwd.h
+++ b/shared/nm-glib-aux/nm-logging-fwd.h
@@ -63,6 +63,14 @@ typedef enum  { /*< skip >*/
 	LOGD_IP         = LOGD_IP4 | LOGD_IP6,
 } NMLogDomain;
 
+static inline NMLogDomain
+LOGD_DHCP_from_addr_family (int addr_family)
+{
+	nm_assert_addr_family (addr_family);
+
+	return addr_family == AF_INET6 ? LOGD_DHCP6 : LOGD_DHCP4;
+}
+
 /* Log levels */
 typedef enum  { /*< skip >*/
 	LOGL_TRACE,
diff --git a/shared/nm-glib-aux/nm-macros-internal.h b/shared/nm-glib-aux/nm-macros-internal.h
index d1ea04c0..d5a8513e 100644
--- a/shared/nm-glib-aux/nm-macros-internal.h
+++ b/shared/nm-glib-aux/nm-macros-internal.h
@@ -337,52 +337,65 @@ _nm_auto_freev (gpointer ptr)
 
 /*****************************************************************************/
 
-/* http://stackoverflow.com/a/11172679 */
-#define  _NM_UTILS_MACRO_FIRST(...)                           __NM_UTILS_MACRO_FIRST_HELPER(__VA_ARGS__, throwaway)
-#define __NM_UTILS_MACRO_FIRST_HELPER(first, ...)             first
-
-#define  _NM_UTILS_MACRO_REST(...)                            __NM_UTILS_MACRO_REST_HELPER(__NM_UTILS_MACRO_REST_NUM(__VA_ARGS__), __VA_ARGS__)
-#define __NM_UTILS_MACRO_REST_HELPER(qty, ...)                __NM_UTILS_MACRO_REST_HELPER2(qty, __VA_ARGS__)
-#define __NM_UTILS_MACRO_REST_HELPER2(qty, ...)               __NM_UTILS_MACRO_REST_HELPER_##qty(__VA_ARGS__)
-#define __NM_UTILS_MACRO_REST_HELPER_ONE(first)
-#define __NM_UTILS_MACRO_REST_HELPER_TWOORMORE(first, ...)    , __VA_ARGS__
-#define __NM_UTILS_MACRO_REST_NUM(...) \
-    __NM_UTILS_MACRO_REST_SELECT_30TH(__VA_ARGS__, \
-                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
-                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
-                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
-                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
-                TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
-                TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway)
-#define __NM_UTILS_MACRO_REST_SELECT_30TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, ...) a30
-
-/*****************************************************************************/
-
-/* http://stackoverflow.com/a/2124385/354393
- * https://stackoverflow.com/questions/11317474/macro-to-count-number-of-arguments
- */
-
-#define NM_NARG(...) \
-         _NM_NARG(, ##__VA_ARGS__, _NM_NARG_RSEQ_N())
-#define _NM_NARG(...) \
-         _NM_NARG_ARG_N(__VA_ARGS__)
-#define _NM_NARG_ARG_N( \
-          _0, \
+#define _NM_MACRO_SELECT_ARG_64( \
           _1, _2, _3, _4, _5, _6, _7, _8, _9,_10, \
          _11,_12,_13,_14,_15,_16,_17,_18,_19,_20, \
          _21,_22,_23,_24,_25,_26,_27,_28,_29,_30, \
          _31,_32,_33,_34,_35,_36,_37,_38,_39,_40, \
          _41,_42,_43,_44,_45,_46,_47,_48,_49,_50, \
          _51,_52,_53,_54,_55,_56,_57,_58,_59,_60, \
-         _61,_62,_63,N,...) N
-#define _NM_NARG_RSEQ_N() \
-         63,62,61,60,                   \
-         59,58,57,56,55,54,53,52,51,50, \
-         49,48,47,46,45,44,43,42,41,40, \
-         39,38,37,36,35,34,33,32,31,30, \
-         29,28,27,26,25,24,23,22,21,20, \
-         19,18,17,16,15,14,13,12,11,10, \
-         9,8,7,6,5,4,3,2,1,0
+         _61,_62,_63, N,...) N
+
+/* http://stackoverflow.com/a/2124385/354393
+ * https://stackoverflow.com/questions/11317474/macro-to-count-number-of-arguments
+ */
+
+#define NM_NARG(...) \
+    _NM_MACRO_SELECT_ARG_64(, ##__VA_ARGS__, \
+                                                 62,61,60, \
+                            59,58,57,56,55,54,53,52,51,50, \
+                            49,48,47,46,45,44,43,42,41,40, \
+                            39,38,37,36,35,34,33,32,31,30, \
+                            29,28,27,26,25,24,23,22,21,20, \
+                            19,18,17,16,15,14,13,12,11,10, \
+                             9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
+#define NM_NARG_MAX1(...) \
+    _NM_MACRO_SELECT_ARG_64(, ##__VA_ARGS__, \
+                                                  1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 1, \
+                             1, 1, 1, 1, 1, 1, 1, 1, 1, 0)
+#define NM_NARG_MAX2(...) \
+    _NM_MACRO_SELECT_ARG_64(, ##__VA_ARGS__, \
+                                                  2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 2, 2, \
+                             2, 2, 2, 2, 2, 2, 2, 2, 1, 0)
+
+#define _NM_MACRO_CALL(macro, ...) macro (__VA_ARGS__)
+
+/*****************************************************************************/
+
+#define _NM_MACRO_COMMA_IF_ARGS(...)       _NM_MACRO_CALL (G_PASTE (__NM_MACRO_COMMA_IF_ARGS_, NM_NARG_MAX1(__VA_ARGS__)), __VA_ARGS__)
+#define __NM_MACRO_COMMA_IF_ARGS_0()
+#define __NM_MACRO_COMMA_IF_ARGS_1(...)    ,
+
+/*****************************************************************************/
+
+/* http://stackoverflow.com/a/11172679 */
+#define  _NM_UTILS_MACRO_FIRST(...)                  __NM_UTILS_MACRO_FIRST_HELPER(__VA_ARGS__, throwaway)
+#define __NM_UTILS_MACRO_FIRST_HELPER(first, ...)    first
+
+#define  _NM_UTILS_MACRO_REST(...)                   _NM_MACRO_CALL (G_PASTE (__NM_UTILS_MACRO_REST_, NM_NARG_MAX2(__VA_ARGS__)), __VA_ARGS__)
+#define __NM_UTILS_MACRO_REST_0()
+#define __NM_UTILS_MACRO_REST_1(first)
+#define __NM_UTILS_MACRO_REST_2(first, ...)          , __VA_ARGS__
 
 /*****************************************************************************/
 
@@ -744,6 +757,17 @@ NM_G_ERROR_MSG (GError *error)
 
 /*****************************************************************************/
 
+#define NM_SWAP(a, b) \
+	G_STMT_START { \
+		typeof (a) _tmp; \
+		\
+		_tmp = (a); \
+		(a) = (b); \
+		(b) = _tmp; \
+	} G_STMT_END
+
+/*****************************************************************************/
+
 static inline gboolean
 _NM_IN_STRSET_streq (const char *x, const char *s)
 {
@@ -980,6 +1004,7 @@ nm_str_realloc (char *str)
 	(cond) ? (str) : (str_else), \
 	(cond) ? (suffix) : ""
 #define NM_PRINT_FMT_QUOTE_STRING(arg) NM_PRINT_FMT_QUOTED((arg), "\"", (arg), "\"", "(null)")
+#define NM_PRINT_FMT_QUOTE_REF_STRING(arg) NM_PRINT_FMT_QUOTED((arg), "\"", (arg)->str, "\"", "(null)")
 
 /*****************************************************************************/
 
@@ -1009,19 +1034,37 @@ nm_str_realloc (char *str)
 #define nm_assert_not_reached() G_STMT_START { ; } G_STMT_END
 #endif
 
+/* Usage:
+ *
+ *   if (NM_MORE_ASSERT_ONCE (5)) { extra_check (); }
+ *
+ * This will only run the check once, and only if NM_MORE_ASSERT is >= than
+ * more_assert_level.
+ */
+#define NM_MORE_ASSERT_ONCE(more_assert_level) \
+	(   (NM_MORE_ASSERTS >= (more_assert_level)) \
+	 && ({ \
+	        static volatile int _assert_once = 0; \
+	        \
+	        G_STATIC_ASSERT_EXPR ((more_assert_level) > 0); \
+	        \
+	        G_UNLIKELY (   _assert_once == 0 \
+	                    && g_atomic_int_compare_and_exchange (&_assert_once, 0, 1)); \
+	    }))
+
 /*****************************************************************************/
 
-#define NM_GOBJECT_PROPERTIES_DEFINE_BASE(...) \
+#define NM_GOBJECT_PROPERTIES_DEFINE_BASE_FULL(suffix, ...) \
 typedef enum { \
-	PROP_0, \
+	PROP_0##suffix, \
 	__VA_ARGS__ \
-	_PROPERTY_ENUMS_LAST, \
-} _PropertyEnums; \
-static GParamSpec *obj_properties[_PROPERTY_ENUMS_LAST] = { NULL, }
+	_PROPERTY_ENUMS_LAST##suffix, \
+} _PropertyEnums##suffix; \
+static GParamSpec *obj_properties##suffix[_PROPERTY_ENUMS_LAST##suffix] = { NULL, }
 
-#define NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY(obj_type, obj_properties, property_enums_type, prop_0) \
+#define NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY(suffix, obj_type) \
 static inline void \
-_nm_gobject_notify_together_impl (obj_type *obj, guint n, const property_enums_type *props) \
+_nm_gobject_notify_together_impl##suffix (obj_type *obj, guint n, const _PropertyEnums##suffix *props) \
 { \
 	const gboolean freeze_thaw = (n > 1); \
 	\
@@ -1031,12 +1074,12 @@ _nm_gobject_notify_together_impl (obj_type *obj, guint n, const property_enums_t
 	if (freeze_thaw) \
 		g_object_freeze_notify ((GObject *) obj); \
 	while (n-- > 0) { \
-		const property_enums_type prop = *props++; \
+		const _PropertyEnums##suffix prop = *props++; \
 		\
-		if (prop != prop_0) { \
-			nm_assert ((gsize) prop < G_N_ELEMENTS (obj_properties)); \
-			nm_assert (obj_properties[prop]); \
-			g_object_notify_by_pspec ((GObject *) obj, obj_properties[prop]); \
+		if (prop != PROP_0##suffix) { \
+			nm_assert ((gsize) prop < G_N_ELEMENTS (obj_properties##suffix)); \
+			nm_assert (obj_properties##suffix[prop]); \
+			g_object_notify_by_pspec ((GObject *) obj, obj_properties##suffix[prop]); \
 		} \
 	} \
 	if (freeze_thaw) \
@@ -1044,20 +1087,29 @@ _nm_gobject_notify_together_impl (obj_type *obj, guint n, const property_enums_t
 } \
 \
 _nm_unused static inline void \
-_notify (obj_type *obj, property_enums_type prop) \
+_notify##suffix (obj_type *obj, _PropertyEnums##suffix prop) \
 { \
-	_nm_gobject_notify_together_impl (obj, 1, &prop); \
+	_nm_gobject_notify_together_impl##suffix (obj, 1, &prop); \
 } \
 
+#define NM_GOBJECT_PROPERTIES_DEFINE_BASE(...) \
+	NM_GOBJECT_PROPERTIES_DEFINE_BASE_FULL (, __VA_ARGS__); \
+
+#define NM_GOBJECT_PROPERTIES_DEFINE_FULL(suffix, obj_type, ...) \
+	NM_GOBJECT_PROPERTIES_DEFINE_BASE_FULL (suffix, __VA_ARGS__); \
+	NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY (suffix, obj_type)
+
 #define NM_GOBJECT_PROPERTIES_DEFINE(obj_type, ...) \
-NM_GOBJECT_PROPERTIES_DEFINE_BASE (__VA_ARGS__); \
-NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY (obj_type, obj_properties, _PropertyEnums, PROP_0)
+	NM_GOBJECT_PROPERTIES_DEFINE_FULL (, obj_type, __VA_ARGS__)
 
 /* invokes _notify() for all arguments (of type _PropertyEnums). Note, that if
  * there are more than one prop arguments, this will involve a freeze/thaw
  * of GObject property notifications. */
+#define nm_gobject_notify_together_full(suffix, obj, ...) \
+	_nm_gobject_notify_together_impl##suffix (obj, NM_NARG (__VA_ARGS__), (const _PropertyEnums##suffix[]) { __VA_ARGS__ })
+
 #define nm_gobject_notify_together(obj, ...) \
-	_nm_gobject_notify_together_impl (obj, NM_NARG (__VA_ARGS__), (const _PropertyEnums[]) { __VA_ARGS__ })
+	nm_gobject_notify_together_full (, obj, __VA_ARGS__)
 
 /*****************************************************************************/
 
@@ -1065,9 +1117,9 @@ NM_GOBJECT_PROPERTIES_DEFINE_NOTIFY (obj_type, obj_properties, _PropertyEnums, P
 #if _NM_CC_SUPPORT_AUTO_TYPE
 #define _NM_GET_PRIVATE_PTR(self, type, is_check, ...) \
 	({ \
-		_nm_auto_type _self = NM_GOBJECT_CAST_NON_NULL (type, (self), is_check, ##__VA_ARGS__); \
+		_nm_auto_type _self_get_private = NM_GOBJECT_CAST_NON_NULL (type, (self), is_check, ##__VA_ARGS__); \
 		\
-		NM_PROPAGATE_CONST (_self, _self->_priv); \
+		NM_PROPAGATE_CONST (_self_get_private, _self_get_private->_priv); \
 	})
 #else
 #define _NM_GET_PRIVATE_PTR(self, type, is_check, ...) (NM_GOBJECT_CAST_NON_NULL (type, (self), is_check, ##__VA_ARGS__)->_priv)
@@ -1348,8 +1400,8 @@ nm_g_variant_take_ref (GVariant *v)
 #define NM_UTILS_LOOKUP_ITEM_IGNORE(v)        (void) 0; case v: break; (void) 0
 #define NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER()   (void) 0; default: break; (void) 0
 
-#define _NM_UTILS_LOOKUP_DEFINE(scope, fcn_name, lookup_type, result_type, unknown_val, ...) \
-scope result_type \
+#define NM_UTILS_LOOKUP_DEFINE(fcn_name, lookup_type, result_type, unknown_val, ...) \
+result_type \
 fcn_name (lookup_type val) \
 { \
 	switch (val) { \
@@ -1361,9 +1413,7 @@ fcn_name (lookup_type val) \
 }
 
 #define NM_UTILS_LOOKUP_STR_DEFINE(fcn_name, lookup_type, unknown_val, ...) \
-	_NM_UTILS_LOOKUP_DEFINE (, fcn_name, lookup_type, const char *, unknown_val, __VA_ARGS__)
-#define NM_UTILS_LOOKUP_STR_DEFINE_STATIC(fcn_name, lookup_type, unknown_val, ...) \
-	_NM_UTILS_LOOKUP_DEFINE (static, fcn_name, lookup_type, const char *, unknown_val, __VA_ARGS__)
+	NM_UTILS_LOOKUP_DEFINE (fcn_name, lookup_type, const char *, unknown_val, __VA_ARGS__)
 
 /* Call the string-lookup-table function @fcn_name. If the function returns
  * %NULL, the numeric index is converted to string using a alloca() buffer.
@@ -1415,6 +1465,14 @@ fcn_name (lookup_type val) \
 			: _flags & (~_val); \
 	})
 
+#define NM_FLAGS_ASSIGN_MASK(flags, mask, val)  ({ \
+		const typeof(flags) _flags = (flags); \
+		const typeof(flags) _mask = (mask); \
+		const typeof(flags) _val = (val); \
+		\
+		((_flags & ~_mask) | (_mask & _val)); \
+	})
+
 /*****************************************************************************/
 
 #define _NM_BACKPORT_SYMBOL_IMPL(version, return_type, orig_func, versioned_func, args_typed, args) \
@@ -1432,7 +1490,12 @@ _NM_BACKPORT_SYMBOL_IMPL(version, return_type, func, _##func##_##version, args_t
 /*****************************************************************************/
 
 /* mirrors g_ascii_isspace() and what we consider spaces in general. */
-#define NM_ASCII_SPACES "\t\n\f\r "
+#define NM_ASCII_SPACES      " \n\t\r\f"
+
+/* Like NM_ASCII_SPACES, but without "\f" (0x0c, Formfeed Page Break).
+ * This is what for example systemd calls WHITESPACE and what it uses to tokenize
+ * the kernel command line. */
+#define NM_ASCII_WHITESPACES " \n\t\r"
 
 #define nm_str_skip_leading_spaces(str) \
 	({ \
@@ -1508,6 +1571,17 @@ nm_strstrip_avoid_copy (const char *str, char **str_free)
 		_str_ssac; \
 	})
 
+static inline gboolean
+nm_str_is_stripped (const char *str)
+{
+	if (str && str[0]) {
+		if (   g_ascii_isspace (str[0])
+		    || g_ascii_isspace (str[strlen (str) - 1]))
+			return FALSE;
+	}
+	return TRUE;
+}
+
 /* g_ptr_array_sort()'s compare function takes pointers to the
  * value. Thus, you cannot use strcmp directly. You can use
  * nm_strcmp_p().
diff --git a/shared/nm-glib-aux/nm-ref-string.h b/shared/nm-glib-aux/nm-ref-string.h
index c80b75ea..60052c45 100644
--- a/shared/nm-glib-aux/nm-ref-string.h
+++ b/shared/nm-glib-aux/nm-ref-string.h
@@ -50,6 +50,18 @@ nm_ref_string_get_len (NMRefString *rstr)
 }
 
 static inline gboolean
+nm_ref_string_equals_str (NMRefString *rstr, const char *s)
+{
+	/* Note that rstr->len might be greater than strlen(rstr->str). This function does
+	 * not cover that and would ignore everything after the first NUL byte. If you need
+	 * that distinction, this function is not for you. */
+
+	return   rstr
+	       ? (s && nm_streq (rstr->str, s))
+	       : (s == NULL);
+}
+
+static inline gboolean
 NM_IS_REF_STRING (const NMRefString *rstr)
 {
 #if NM_MORE_ASSERTS > 10
diff --git a/shared/nm-glib-aux/nm-secret-utils.c b/shared/nm-glib-aux/nm-secret-utils.c
index 5b0afe46..78369b5e 100644
--- a/shared/nm-glib-aux/nm-secret-utils.c
+++ b/shared/nm-glib-aux/nm-secret-utils.c
@@ -8,13 +8,15 @@
 
 #include "nm-secret-utils.h"
 
+#include <malloc.h>
+
 /*****************************************************************************/
 
 void
 nm_explicit_bzero (void *s, gsize n)
 {
 	/* gracefully handle n == 0. This is important, callers rely on it. */
-	if (n == 0)
+	if (G_UNLIKELY (n == 0))
 		return;
 
 	nm_assert (s);
@@ -32,6 +34,30 @@ nm_explicit_bzero (void *s, gsize n)
 #endif
 }
 
+void
+nm_free_secret (char *secret)
+{
+	gsize len;
+
+	if (!secret)
+		return;
+
+#if GLIB_CHECK_VERSION(2,44,0)
+	/* Here we mix malloc() and g_malloc() API. Usually we avoid this,
+	 * however since glib 2.44.0 we are in fact guaranteed that g_malloc()/g_free()
+	 * just wraps malloc()/free(), so this is actually fine.
+	 *
+	 * See https://gitlab.gnome.org/GNOME/glib/commit/3be6ed60aa58095691bd697344765e715a327fc1
+	 */
+	len = malloc_usable_size (secret);
+#else
+	len = strlen (secret);
+#endif
+
+	nm_explicit_bzero (secret, len);
+	g_free (secret);
+}
+
 /*****************************************************************************/
 
 char *
diff --git a/shared/nm-glib-aux/nm-secret-utils.h b/shared/nm-glib-aux/nm-secret-utils.h
index 1b98b7e9..501e7520 100644
--- a/shared/nm-glib-aux/nm-secret-utils.h
+++ b/shared/nm-glib-aux/nm-secret-utils.h
@@ -18,14 +18,7 @@ char *nm_secret_strchomp (char *secret);
 
 /*****************************************************************************/
 
-static inline void
-nm_free_secret (char *secret)
-{
-	if (secret) {
-		nm_explicit_bzero (secret, strlen (secret));
-		g_free (secret);
-	}
-}
+void nm_free_secret (char *secret);
 
 NM_AUTO_DEFINE_FCN0 (char *, _nm_auto_free_secret, nm_free_secret)
 /**
@@ -159,4 +152,125 @@ GBytes *nm_secret_buf_to_gbytes_take (NMSecretBuf *secret, gssize actual_len);
 
 gboolean nm_utils_memeqzero_secret (gconstpointer data, gsize length);
 
+/*****************************************************************************/
+
+/**
+ * nm_secret_mem_realloc:
+ * @m_old: the current buffer of length @cur_len.
+ * @do_bzero_mem: if %TRUE, bzero the old buffer
+ * @cur_len: the current buffer length of @m_old. It is necessary for bzero.
+ * @new_len: the desired new length
+ *
+ * If @do_bzero_mem is false, this is like g_realloc().
+ * Otherwise, this will allocate a new buffer of the desired size, copy over the
+ * old data, and bzero the old buffer before freeing it. As such, it also behaves
+ * similar to g_realloc(), with the overhead of nm_explicit_bzero() and using
+ * malloc/free intead of realloc().
+ *
+ * Returns: the new allocated buffer. Think of it behaving like g_realloc().
+ */
+static inline gpointer
+nm_secret_mem_realloc (gpointer m_old, gboolean do_bzero_mem, gsize cur_len, gsize new_len)
+{
+	gpointer m_new;
+
+	nm_assert (m_old || cur_len == 0);
+
+	if (   do_bzero_mem
+	    && G_LIKELY (cur_len > 0)) {
+		m_new = g_malloc (new_len);
+		if (G_LIKELY (new_len > 0))
+			memcpy (m_new, m_old, NM_MIN (cur_len, new_len));
+		nm_explicit_bzero (m_old, cur_len);
+		g_free (m_old);
+	} else
+		m_new = g_realloc (m_old, new_len);
+
+	return m_new;
+}
+
+/**
+ * nm_secret_mem_try_realloc:
+ * @m_old: the current buffer of length @cur_len.
+ * @do_bzero_mem: if %TRUE, bzero the old buffer
+ * @cur_len: the current buffer length of @m_old. It is necessary for bzero.
+ * @new_len: the desired new length
+ *
+ * If @do_bzero_mem is false, this is like g_try_realloc().
+ * Otherwise, this will try to allocate a new buffer of the desired size, copy over the
+ * old data, and bzero the old buffer before freeing it. As such, it also behaves
+ * similar to g_try_realloc(), with the overhead of nm_explicit_bzero() and using
+ * malloc/free intead of realloc().
+ *
+ * Returns: the new allocated buffer or NULL. Think of it behaving like g_try_realloc().
+ */
+static inline gpointer
+nm_secret_mem_try_realloc (gpointer m_old, gboolean do_bzero_mem, gsize cur_len, gsize new_len)
+{
+	gpointer m_new;
+
+	nm_assert (m_old || cur_len == 0);
+
+	if (   do_bzero_mem
+	    && G_LIKELY (cur_len > 0)) {
+		if (G_UNLIKELY (new_len == 0))
+			m_new = NULL;
+		else {
+			m_new = g_try_malloc (new_len);
+			if (!m_new)
+				return NULL;
+			memcpy (m_new, m_old, NM_MIN (cur_len, new_len));
+		}
+		nm_explicit_bzero (m_old, cur_len);
+		g_free (m_old);
+		return m_new;
+	}
+
+	return g_try_realloc (m_old, new_len);
+}
+
+/**
+ * nm_secret_mem_try_realloc_take:
+ * @m_old: the current buffer of length @cur_len.
+ * @do_bzero_mem: if %TRUE, bzero the old buffer
+ * @cur_len: the current buffer length of @m_old. It is necessary for bzero.
+ * @new_len: the desired new length
+ *
+ * This works like nm_secret_mem_try_realloc(), which is not unlike g_try_realloc().
+ * The difference is, if we fail to allocate a new buffer, then @m_old will be
+ * freed (and possibly cleared). This differs from plain realloc(), where the
+ * old buffer is unchanged if the operation fails.
+ *
+ * Returns: the new allocated buffer or NULL. Think of it behaving like g_try_realloc()
+ *   but it will always free @m_old.
+ */
+static inline gpointer
+nm_secret_mem_try_realloc_take (gpointer m_old, gboolean do_bzero_mem, gsize cur_len, gsize new_len)
+{
+	gpointer m_new;
+
+	nm_assert (m_old || cur_len == 0);
+
+	if (   do_bzero_mem
+	    && G_LIKELY (cur_len > 0)) {
+		if (G_UNLIKELY (new_len == 0))
+			m_new = NULL;
+		else {
+			m_new = g_try_malloc (new_len);
+			if (G_LIKELY (m_new))
+				memcpy (m_new, m_old, NM_MIN (cur_len, new_len));
+		}
+		nm_explicit_bzero (m_old, cur_len);
+		g_free (m_old);
+		return m_new;
+	}
+
+	m_new = g_try_realloc (m_old, new_len);
+	if (G_UNLIKELY (!m_new && new_len > 0))
+		g_free (m_old);
+	return m_new;
+}
+
+/*****************************************************************************/
+
 #endif /* __NM_SECRET_UTILS_H__ */
diff --git a/shared/nm-glib-aux/nm-shared-utils.c b/shared/nm-glib-aux/nm-shared-utils.c
index d47c465c..c06399dd 100644
--- a/shared/nm-glib-aux/nm-shared-utils.c
+++ b/shared/nm-glib-aux/nm-shared-utils.c
@@ -15,6 +15,7 @@
 #include <net/if.h>
 
 #include "nm-errno.h"
+#include "nm-str-buf.h"
 
 /*****************************************************************************/
 
@@ -84,6 +85,75 @@ nm_ip_addr_set_from_untrusted (int addr_family,
 
 /*****************************************************************************/
 
+gsize
+nm_utils_get_next_realloc_size (gboolean true_realloc, gsize requested)
+{
+	gsize n, x;
+
+	/* https://doc.qt.io/qt-5/containers.html#growth-strategies */
+
+	if (requested <= 40) {
+		/* small allocations. Increase in small steps of 8 bytes.
+		 *
+		 * We get thus sizes of 8, 16, 32, 40. */
+		if (requested <= 8)
+			return 8;
+		if (requested <= 16)
+			return 16;
+		if (requested <= 32)
+			return 32;
+
+		/* The return values for < 104 are essentially hard-coded, and the choice here is
+		 * made without very strong reasons.
+		 *
+		 * We want to stay 24 bytes below the power-of-two border 64. Hence, return 40 here.
+		 * However, the next step then is already 104 (128 - 24). It's a larger gap than in
+		 * the steps before.
+		 *
+		 * It's not clear whether some of the steps should be adjusted (or how exactly). */
+		return 40;
+	}
+
+	if (   requested <= 0x2000u - 24u
+	    || G_UNLIKELY (!true_realloc)) {
+		/* mid sized allocations. Return next power of two, minus 24 bytes extra space
+		 * at the beginning.
+		 * That means, we double the size as we grow.
+		 *
+		 * With !true_realloc, it means that the caller does not intend to call
+		 * realloc() but instead clone the buffer. This is for example the case, when we
+		 * want to nm_explicit_bzero() the old buffer. In that case we really want to grow
+		 * the buffer exponentially every time and not increment in page sizes of 4K (below).
+		 *
+		 * We get thus sizes of 104, 232, 488, 1000, 2024, 4072, 8168... */
+
+		if (G_UNLIKELY (requested > G_MAXSIZE / 2u - 24u))
+			return G_MAXSIZE;
+
+		x = requested + 24u;
+		n = 128u;
+		while (n < x) {
+			n <<= 1;
+			nm_assert (n > 128u);
+		}
+
+		nm_assert (n > 24u && n - 24u >= requested);
+		return n - 24u;
+	}
+
+	if (G_UNLIKELY (requested > G_MAXSIZE - 0x1000u - 24u))
+		return G_MAXSIZE;
+
+	/* For large allocations (with !true_realloc) we allocate memory in chunks of
+	 * 4K (- 24 bytes extra), assuming that the memory gets mmapped and thus
+	 * realloc() is efficient by just reordering pages. */
+	n = ((requested + (0x0FFFu + 24u)) & ~((gsize) 0x0FFFu)) - 24u;
+	nm_assert (n >= requested);
+	return n;
+}
+
+/*****************************************************************************/
+
 pid_t
 nm_utils_gettid (void)
 {
@@ -374,6 +444,62 @@ nm_utils_gbytes_to_variant_ay (GBytes *bytes)
 
 /*****************************************************************************/
 
+/* Convert a hash table with "char *" keys and values to an "a{ss}" GVariant.
+ * The keys will be sorted asciibetically.
+ * Returns a floating reference.
+ */
+GVariant *
+nm_utils_strdict_to_variant_ass (GHashTable *strdict)
+{
+	GHashTableIter iter;
+	const char *key, *value;
+	GVariantBuilder builder;
+	guint i, len;
+
+	g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}"));
+
+	if (!strdict)
+		goto out;
+	len = g_hash_table_size (strdict);
+	if (!len)
+		goto out;
+
+	g_hash_table_iter_init (&iter, strdict);
+	if (!g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value))
+		nm_assert_not_reached ();
+
+	if (len == 1)
+		g_variant_builder_add (&builder, "{ss}", key, value);
+	else {
+		gs_free NMUtilsNamedValue *idx_free = NULL;
+		NMUtilsNamedValue *idx;
+
+		if (len > 300 / sizeof (NMUtilsNamedValue)) {
+			idx_free = g_new (NMUtilsNamedValue, len);
+			idx = idx_free;
+		} else
+			idx = g_alloca (sizeof (NMUtilsNamedValue) * len);
+
+		i = 0;
+		do {
+			idx[i].name = key;
+			idx[i].value_str = value;
+			i++;
+		} while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value));
+		nm_assert (i == len);
+
+		nm_utils_named_value_list_sort (idx, len, NULL, NULL);
+
+		for (i = 0; i < len; i++)
+			g_variant_builder_add (&builder, "{ss}", idx[i].name, idx[i].value_str);
+	}
+
+out:
+	return g_variant_builder_end (&builder);
+}
+
+/*****************************************************************************/
+
 /**
  * nm_strquote:
  * @buf: the output buffer of where to write the quoted @str argument.
@@ -685,7 +811,7 @@ nm_utils_ip_is_site_local (int addr_family,
 /*****************************************************************************/
 
 static gboolean
-_parse_legacy_addr4 (const char *text, in_addr_t *out_addr)
+_parse_legacy_addr4 (const char *text, in_addr_t *out_addr, GError **error)
 {
 	gs_free char *s_free = NULL;
 	struct in_addr a1;
@@ -693,8 +819,13 @@ _parse_legacy_addr4 (const char *text, in_addr_t *out_addr)
 	char *s;
 	int i;
 
-	if (inet_aton (text, &a1) != 1)
+	if (inet_aton (text, &a1) != 1) {
+		g_set_error_literal (error,
+		                     NM_UTILS_ERROR,
+		                     NM_UTILS_ERROR_INVALID_ARGUMENT,
+		                     "address invalid according to inet_aton()");
 		return FALSE;
+	}
 
 	/* OK, inet_aton() accepted the format. That's good, because we want
 	 * to accept IPv4 addresses in octal format, like 255.255.000.000.
@@ -711,6 +842,10 @@ _parse_legacy_addr4 (const char *text, in_addr_t *out_addr)
 	if (NM_STRCHAR_ANY (text, ch, (   !(ch >= '0' && ch <= '9')
 	                               && !NM_IN_SET (ch, '.', 'x')))) {
 		/* We only accepts '.', digits, and 'x' for "0x". */
+		g_set_error_literal (error,
+		                     NM_UTILS_ERROR,
+		                     NM_UTILS_ERROR_INVALID_ARGUMENT,
+		                     "contains an invalid character");
 		return FALSE;
 	}
 
@@ -729,13 +864,27 @@ _parse_legacy_addr4 (const char *text, in_addr_t *out_addr)
 		if ((i == G_N_ELEMENTS (bin) - 1) != (s == NULL)) {
 			/* Exactly for the last digit, we expect to have no more following token.
 			 * But this isn't the case. Abort. */
+			g_set_error (error,
+			             NM_UTILS_ERROR,
+			             NM_UTILS_ERROR_INVALID_ARGUMENT,
+			             "wrong number of tokens (index %d, token '%s')",
+			             i, s);
 			return FALSE;
 		}
 
 		v = _nm_utils_ascii_str_to_int64 (current_token, 0, 0, 0xFF, -1);
 		if (v == -1) {
+			int errsv = errno;
+
 			/* we do accept octal and hex (even with leading "0x"). But something
 			 * about this token is wrong. */
+			g_set_error (error,
+			             NM_UTILS_ERROR,
+			             NM_UTILS_ERROR_INVALID_ARGUMENT,
+			             "invalid token '%s': %s (%d)",
+			             current_token,
+			             nm_strerror_native (errsv),
+			             errsv);
 			return FALSE;
 		}
 
@@ -745,6 +894,12 @@ _parse_legacy_addr4 (const char *text, in_addr_t *out_addr)
 	if (memcmp (bin, &a1, sizeof (bin)) != 0) {
 		/* our parsing did not agree with what inet_aton() gave. Something
 		 * is wrong. Abort. */
+		g_set_error (error,
+		             NM_UTILS_ERROR,
+		             NM_UTILS_ERROR_INVALID_ARGUMENT,
+		             "inet_aton() result 0x%08x differs from computed value 0x%02hhx%02hhx%02hhx%02hhx",
+		             a1.s_addr,
+		             bin[0], bin[1], bin[2], bin[3]);
 		return FALSE;
 	}
 
@@ -772,7 +927,7 @@ nm_utils_parse_inaddr_bin_full (int addr_family,
 	if (inet_pton (addr_family, text, &addrbin) != 1) {
 		if (   accept_legacy
 		    && addr_family == AF_INET
-		    && _parse_legacy_addr4 (text, &addrbin.addr4)) {
+		    && _parse_legacy_addr4 (text, &addrbin.addr4, NULL)) {
 			/* The address is in some legacy format which inet_aton() accepts, but not inet_pton().
 			 * Most likely octal digits (leading zeros). We accept the address. */
 		} else
@@ -781,11 +936,17 @@ nm_utils_parse_inaddr_bin_full (int addr_family,
 
 #if NM_MORE_ASSERTS > 10
 	if (addr_family == AF_INET) {
+		gs_free_error GError *error = NULL;
 		in_addr_t a;
 
 		/* The legacy parser should accept everything that inet_pton() accepts too. Meaning,
 		 * it should strictly parse *more* formats. And of course, parse it the same way. */
-		nm_assert (_parse_legacy_addr4 (text, &a));
+		if (!_parse_legacy_addr4 (text, &a, &error)) {
+			char buf[INET_ADDRSTRLEN];
+
+			g_error ("unexpected assertion failure: could parse \"%s\" as %s, but not accepted by legacy parser: %s",
+			         text, _nm_utils_inet4_ntop (addrbin.addr4, buf), error->message);
+		}
 		nm_assert (addrbin.addr4 == a);
 	}
 #endif
@@ -883,6 +1044,195 @@ nm_utils_parse_inaddr_prefix (int addr_family,
 
 /*****************************************************************************/
 
+gboolean
+nm_utils_ipaddr_is_valid (int addr_family,
+                          const char *str_addr)
+{
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	return    str_addr
+	       && nm_utils_parse_inaddr_bin (addr_family,
+	                                     str_addr,
+	                                     NULL,
+	                                     NULL);
+}
+
+gboolean
+nm_utils_ipaddr_is_normalized (int addr_family,
+                               const char *str_addr)
+{
+	NMIPAddr addr;
+	char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+	nm_assert (NM_IN_SET (addr_family, AF_UNSPEC, AF_INET, AF_INET6));
+
+	if (!str_addr)
+		return FALSE;
+
+	if (!nm_utils_parse_inaddr_bin (addr_family,
+	                                str_addr,
+	                                &addr_family,
+	                                &addr))
+		return FALSE;
+
+	nm_utils_inet_ntop (addr_family, &addr, sbuf);
+	return nm_streq (sbuf, str_addr);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_g_ascii_strtoll()
+ * @nptr: the string to parse
+ * @endptr: the pointer on the first invalid chars
+ * @base: the base.
+ *
+ * This wraps g_ascii_strtoll() and should in almost all cases behave identical
+ * to it.
+ *
+ * However, it seems there are situations where g_ascii_strtoll() might set
+ * errno to some unexpected value EAGAIN. Possibly this is related to creating
+ * the C locale during
+ *
+ *   #ifdef USE_XLOCALE
+ *   return strtoll_l (nptr, endptr, base, get_C_locale ());
+ *
+ * This wrapper tries to workaround that condition.
+ */
+gint64
+nm_g_ascii_strtoll (const char *nptr,
+                    char **endptr,
+                    guint base)
+{
+	int try_count = 2;
+	gint64 v;
+	const int errsv_orig = errno;
+	int errsv;
+
+	nm_assert (nptr);
+	nm_assert (base == 0u || (base >= 2u && base <= 36u));
+
+again:
+	errno = 0;
+	v = g_ascii_strtoll (nptr, endptr, base);
+	errsv = errno;
+
+	if (errsv == 0) {
+		if (errsv_orig != 0)
+			errno = errsv_orig;
+		return v;
+	}
+
+	if (   errsv == ERANGE
+	    && NM_IN_SET (v, G_MININT64, G_MAXINT64))
+		return v;
+
+	if (   errsv == EINVAL
+	    && v == 0
+	    && nptr
+	    && nptr[0] == '\0')
+		return v;
+
+	if (try_count-- > 0)
+		goto again;
+
+#if NM_MORE_ASSERTS
+	g_critical ("g_ascii_strtoll() for \"%s\" failed with errno=%d (%s) and v=%"G_GINT64_FORMAT,
+	            nptr,
+	            errsv,
+	            nm_strerror_native (errsv),
+	            v);
+#endif
+
+	return v;
+}
+
+/* See nm_g_ascii_strtoll() */
+guint64
+nm_g_ascii_strtoull (const char *nptr,
+                     char **endptr,
+                     guint base)
+{
+	int try_count = 2;
+	guint64 v;
+	const int errsv_orig = errno;
+	int errsv;
+
+	nm_assert (nptr);
+	nm_assert (base == 0u || (base >= 2u && base <= 36u));
+
+again:
+	errno = 0;
+	v = g_ascii_strtoull (nptr, endptr, base);
+	errsv = errno;
+
+	if (errsv == 0) {
+		if (errsv_orig != 0)
+			errno = errsv_orig;
+		return v;
+	}
+
+	if (   errsv == ERANGE
+	    && NM_IN_SET (v, G_MAXUINT64))
+		return v;
+
+	if (   errsv == EINVAL
+	    && v == 0
+	    && nptr
+	    && nptr[0] == '\0')
+		return v;
+
+	if (try_count-- > 0)
+		goto again;
+
+#if NM_MORE_ASSERTS
+	g_critical ("g_ascii_strtoull() for \"%s\" failed with errno=%d (%s) and v=%"G_GUINT64_FORMAT,
+	            nptr,
+	            errsv,
+	            nm_strerror_native (errsv),
+	            v);
+#endif
+
+	return v;
+}
+
+/* see nm_g_ascii_strtoll(). */
+double
+nm_g_ascii_strtod (const char *nptr,
+                   char **endptr)
+{
+	int try_count = 2;
+	double v;
+	int errsv;
+
+	nm_assert (nptr);
+
+again:
+	v = g_ascii_strtod (nptr, endptr);
+	errsv = errno;
+
+	if (errsv == 0)
+		return v;
+
+	if (errsv == ERANGE)
+		return v;
+
+	if (try_count-- > 0)
+		goto again;
+
+#if NM_MORE_ASSERTS
+	g_critical ("g_ascii_strtod() for \"%s\" failed with errno=%d (%s) and v=%f",
+	            nptr,
+	            errsv,
+	            nm_strerror_native (errsv),
+	            v);
+#endif
+
+	/* Not really much else to do. Return the parsed value and leave errno set
+	 * to the unexpected value. */
+	return v;
+}
+
 /* _nm_utils_ascii_str_to_int64:
  *
  * A wrapper for g_ascii_strtoll, that checks whether the whole string
@@ -910,7 +1260,7 @@ _nm_utils_ascii_str_to_int64 (const char *str, guint base, gint64 min, gint64 ma
 	}
 
 	errno = 0;
-	v = g_ascii_strtoll (str, (char **) &s, base);
+	v = nm_g_ascii_strtoll (str, (char **) &s, base);
 
 	if (errno != 0)
 		return fallback;
@@ -946,7 +1296,7 @@ _nm_utils_ascii_str_to_uint64 (const char *str, guint base, guint64 min, guint64
 	}
 
 	errno = 0;
-	v = g_ascii_strtoull (str, (char **) &s, base);
+	v = nm_g_ascii_strtoull (str, (char **) &s, base);
 
 	if (errno != 0)
 		return fallback;
@@ -965,8 +1315,8 @@ _nm_utils_ascii_str_to_uint64 (const char *str, guint base, guint64 min, guint64
 
 	if (   v != 0
 	    && str[0] == '-') {
-		/* I don't know why, but g_ascii_strtoull() accepts minus signs ("-2" gives 18446744073709551614).
-		 * For "-0" that is OK, but otherwise not. */
+		/* As documented, g_ascii_strtoull() accepts negative values, and returns their
+		 * absolute value. We don't. */
 		errno = ERANGE;
 		return fallback;
 	}
@@ -1147,12 +1497,27 @@ comp_l:
 /*****************************************************************************/
 
 static void
+_char_lookup_table_set_one (guint8 lookup[static 256],
+                             char ch)
+{
+	lookup[(guint8) ch] = 1;
+}
+
+static void
+_char_lookup_table_set_all (guint8 lookup[static 256],
+                            const char *candidates)
+{
+	while (candidates[0] != '\0')
+		_char_lookup_table_set_one (lookup, (candidates++)[0]);
+}
+
+static void
 _char_lookup_table_init (guint8 lookup[static 256],
                          const char *candidates)
 {
 	memset (lookup, 0, 256);
-	while (candidates[0] != '\0')
-		lookup[(guint8) ((candidates++)[0])] = 1;
+	if (candidates)
+		_char_lookup_table_set_all (lookup, candidates);
 }
 
 static gboolean
@@ -1163,6 +1528,19 @@ _char_lookup_has (const guint8 lookup[static 256],
 	return lookup[(guint8) ch] != 0;
 }
 
+static gboolean
+_char_lookup_has_all (const guint8 lookup[static 256],
+                      const char *candidates)
+{
+	if (candidates) {
+		while (candidates[0] != '\0') {
+			if (!_char_lookup_has (lookup, (candidates++)[0]))
+				return FALSE;
+		}
+	}
+	return TRUE;
+}
+
 /**
  * nm_utils_strsplit_set_full:
  * @str: the string to split.
@@ -1175,18 +1553,7 @@ _char_lookup_has (const guint8 lookup[static 256],
  *
  * Note that for @str %NULL and "", this always returns %NULL too. That differs
  * from g_strsplit_set(), which would return an empty strv array for "".
- *
- * Note that g_strsplit_set() returns empty words as well. By default,
- * nm_utils_strsplit_set_full() strips all empty tokens (that is, repeated
- * delimiters. With %NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY, empty tokens
- * are not removed.
- *
- * If @flags has %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING, delimiters prefixed
- * by a backslash are not treated as a separator. Such delimiters and their escape
- * character are copied to the current word without unescaping them. In general,
- * nm_utils_strsplit_set_full() does not remove any backslash escape characters
- * and does not unescaping. It only considers them for skipping to split at
- * an escaped delimiter.
+ * This never returns an empty array.
  *
  * Returns: %NULL if @str is %NULL or "".
  *   If @str only contains delimiters and %NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY
@@ -1196,7 +1563,7 @@ _char_lookup_has (const guint8 lookup[static 256],
  *   The strings to which the result strv array points to are allocated
  *   after the returned result itself. Don't free the strings themself,
  *   but free everything with g_free().
- *   It is however safe and allowed to modify the indiviual strings,
+ *   It is however safe and allowed to modify the individual strings in-place,
  *   like "g_strstrip((char *) iter[0])".
  */
 const char **
@@ -1378,11 +1745,9 @@ done2:
 
 		/* We no longer need ch_lookup for its original purpose. Modify it, so it
 		 * can detect the delimiters, '\\', and (optionally) whitespaces. */
-		ch_lookup[((guint8) '\\')] = 1;
-		if (f_strstrip) {
-			for (i = 0; NM_ASCII_SPACES[i]; i++)
-				ch_lookup[((guint8) (NM_ASCII_SPACES[i]))] = 1;
-		}
+		_char_lookup_table_set_one (ch_lookup, '\\');
+		if (f_strstrip)
+			_char_lookup_table_set_all (ch_lookup, NM_ASCII_SPACES);
 
 		for (i_token = 0; ptr[i_token]; i_token++) {
 			s = (char *) ptr[i_token];
@@ -1403,65 +1768,131 @@ done2:
 /*****************************************************************************/
 
 const char *
-nm_utils_escaped_tokens_escape (const char *str,
-                                const char *delimiters,
-                                char **out_to_free)
+nm_utils_escaped_tokens_escape_full (const char *str,
+                                     const char *delimiters,
+                                     const char *delimiters_as_needed,
+                                     NMUtilsEscapedTokensEscapeFlags flags,
+                                     char **out_to_free)
 {
 	guint8 ch_lookup[256];
+	guint8 ch_lookup_as_needed[256];
+	gboolean has_ch_lookup_as_needed = FALSE;
 	char *ret;
 	gsize str_len;
 	gsize alloc_len;
 	gsize n_escapes;
 	gsize i, j;
+	gboolean escape_leading_space;
 	gboolean escape_trailing_space;
+	gboolean escape_backslash_as_needed;
 
-	if (!delimiters) {
-		nm_assert (delimiters);
-		delimiters = NM_ASCII_SPACES;
-	}
+	nm_assert (   !delimiters_as_needed
+	           || (   delimiters_as_needed[0]
+	               && NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED)));
 
 	if (!str || str[0] == '\0') {
 		*out_to_free = NULL;
 		return str;
 	}
 
+	str_len = strlen (str);
+
 	_char_lookup_table_init (ch_lookup, delimiters);
+	if (   !delimiters
+	    || NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES)) {
+		flags &= ~(  NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
+		           | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE);
+		_char_lookup_table_set_all (ch_lookup, NM_ASCII_SPACES);
+	}
 
-	/* also mark '\\' as requiring escaping. */
-	ch_lookup[((guint8) '\\')] = 1;
+	if (NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS)) {
+		_char_lookup_table_set_one (ch_lookup, '\\');
+		escape_backslash_as_needed = FALSE;
+	} else if (_char_lookup_has (ch_lookup, '\\'))
+		escape_backslash_as_needed = FALSE;
+	else {
+		escape_backslash_as_needed = NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED);
+		if (escape_backslash_as_needed) {
+			if (    NM_FLAGS_ANY (flags,   NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
+			                             | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE)
+			    && !_char_lookup_has_all (ch_lookup, NM_ASCII_SPACES)) {
+				/* ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE implies that we escape backslash
+				 * before whitespaces. */
+				if (!has_ch_lookup_as_needed) {
+					has_ch_lookup_as_needed = TRUE;
+					_char_lookup_table_init (ch_lookup_as_needed, NULL);
+				}
+				_char_lookup_table_set_all (ch_lookup_as_needed, NM_ASCII_SPACES);
+			}
+			if (   delimiters_as_needed
+			    && !_char_lookup_has_all (ch_lookup, delimiters_as_needed)) {
+				if (!has_ch_lookup_as_needed) {
+					has_ch_lookup_as_needed = TRUE;
+					_char_lookup_table_init (ch_lookup_as_needed, NULL);
+				}
+				_char_lookup_table_set_all (ch_lookup_as_needed, delimiters_as_needed);
+			}
+		}
+	}
+
+	escape_leading_space =    NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE)
+	                       && g_ascii_isspace (str[0])
+	                       && !_char_lookup_has (ch_lookup, str[0]);
+	if (str_len == 1)
+		escape_trailing_space = FALSE;
+	else {
+		escape_trailing_space =    NM_FLAGS_HAS (flags, NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE)
+		                        && g_ascii_isspace (str[str_len - 1])
+		                        && !_char_lookup_has (ch_lookup, str[str_len - 1]);
+	}
 
 	n_escapes = 0;
 	for (i = 0; str[i] != '\0'; i++) {
 		if (_char_lookup_has (ch_lookup, str[i]))
 			n_escapes++;
+		else if (   str[i] == '\\'
+		         && escape_backslash_as_needed
+		         && (   _char_lookup_has (ch_lookup, str[i + 1])
+		             || NM_IN_SET (str[i + 1], '\0', '\\')
+		             || (   has_ch_lookup_as_needed
+		                 && _char_lookup_has (ch_lookup_as_needed, str[i + 1]))))
+			n_escapes++;
 	}
+	if (escape_leading_space)
+		n_escapes++;
+	if (escape_trailing_space)
+		n_escapes++;
 
-	str_len = i;
-	nm_assert (str_len > 0 && strlen (str) == str_len);
-
-	escape_trailing_space =    !_char_lookup_has (ch_lookup, str[str_len - 1])
-	                        && g_ascii_isspace (str[str_len - 1]);
-
-	if (   n_escapes == 0
-	    && !escape_trailing_space) {
+	if (n_escapes == 0u) {
 		*out_to_free = NULL;
 		return str;
 	}
 
-	alloc_len = str_len + n_escapes + ((gsize) escape_trailing_space) + 1;
+	alloc_len = str_len + n_escapes + 1u;
 	ret = g_new (char, alloc_len);
 
 	j = 0;
-	for (i = 0; str[i] != '\0'; i++) {
-		if (_char_lookup_has (ch_lookup, str[i])) {
-			nm_assert (j < alloc_len);
+	i = 0;
+
+	if (escape_leading_space) {
+		ret[j++] = '\\';
+		ret[j++] = str[i++];
+	}
+	for (; str[i] != '\0'; i++) {
+		if (_char_lookup_has (ch_lookup, str[i]))
+			ret[j++] = '\\';
+		else if (   str[i] == '\\'
+		         && escape_backslash_as_needed
+		         && (   _char_lookup_has (ch_lookup, str[i + 1])
+		             || NM_IN_SET (str[i + 1], '\0', '\\')
+		             || (   has_ch_lookup_as_needed
+		                 && _char_lookup_has (ch_lookup_as_needed, str[i + 1]))))
 			ret[j++] = '\\';
-		}
-		nm_assert (j < alloc_len);
 		ret[j++] = str[i];
 	}
 	if (escape_trailing_space) {
-		nm_assert (!_char_lookup_has (ch_lookup, ret[j - 1]) && g_ascii_isspace (ret[j - 1]));
+		nm_assert (   !_char_lookup_has (ch_lookup, ret[j - 1])
+		           && g_ascii_isspace (ret[j - 1]));
 		ret[j] = ret[j - 1];
 		ret[j - 1] = '\\';
 		j++;
@@ -1469,11 +1900,97 @@ nm_utils_escaped_tokens_escape (const char *str,
 
 	nm_assert (j == alloc_len - 1);
 	ret[j] = '\0';
+	nm_assert (strlen (ret) == j);
 
 	*out_to_free = ret;
 	return ret;
 }
 
+/**
+ * nm_utils_escaped_tokens_options_split:
+ * @str: the src string. This string will be modified in-place.
+ *   The output values will point into @str.
+ * @out_key: (allow-none): the returned output key. This will always be set to @str
+ *   itself. @str will be modified to contain only the unescaped, truncated
+ *   key name.
+ * @out_val: returns the parsed (and unescaped) value or %NULL, if @str contains
+ *   no '=' delimiter.
+ *
+ * Honors backslash escaping to parse @str as "key=value" pairs. Optionally, if no '='
+ * is present, @out_val will be returned as %NULL. Backslash can be used to escape
+ * '=', ',', '\\', and ascii whitespace. Other backslash sequences are taken verbatim.
+ *
+ * For keys, '=' obviously must be escaped. For values, that is optional because an
+ * unescaped '=' is just taken verbatim. For example, in a key, the sequence "\\="
+ * must be escaped as "\\\\\\=". For the value, that works too, but "\\\\=" is also
+ * accepted.
+ *
+ * Unescaped Space around the key and value are also removed. Space in general must
+ * not be escaped, unless they are at the beginning or the end of key/value.
+ */
+void
+nm_utils_escaped_tokens_options_split (char *str,
+                                       const char **out_key,
+                                       const char **out_val)
+{
+	const char *val = NULL;
+	gsize i;
+	gsize j;
+	gsize last_space_idx;
+	gboolean last_space_has;
+
+	nm_assert (str);
+
+	i = 0;
+	while (g_ascii_isspace (str[i]))
+		i++;
+
+	j = 0;
+	last_space_idx = 0;
+	last_space_has = FALSE;
+	while (str[i] != '\0') {
+		if (g_ascii_isspace (str[i])) {
+			if (!last_space_has) {
+				last_space_has = TRUE;
+				last_space_idx = j;
+			}
+		} else {
+			if (str[i] == '\\') {
+				if (   NM_IN_SET (str[i + 1u], '\\', ',', '=')
+				    || g_ascii_isspace (str[i + 1u]))
+					i++;
+			} else if (str[i] == '=') {
+				/* Encounter an unescaped '=' character. When we still parse the key, this
+				 * is the separator we were waiting for. If we are parsing the value,
+				 * we take the character verbatim. */
+				if (!val) {
+					if (last_space_has) {
+						str[last_space_idx] = '\0';
+						j = last_space_idx + 1;
+						last_space_has = FALSE;
+					} else
+						str[j++] = '\0';
+					val = &str[j];
+					i++;
+					while (g_ascii_isspace (str[i]))
+						i++;
+					continue;
+				}
+			}
+			last_space_has = FALSE;
+		}
+		str[j++] = str[i++];
+	}
+
+	if (last_space_has)
+		str[last_space_idx] = '\0';
+	else
+		str[j] = '\0';
+
+	*out_key = str;
+	*out_val = val;
+}
+
 /*****************************************************************************/
 
 /**
@@ -1603,16 +2120,13 @@ nm_utils_error_set_cancelled (GError **error,
 }
 
 gboolean
-nm_utils_error_is_cancelled (GError *error,
-                             gboolean consider_is_disposing)
+nm_utils_error_is_cancelled_or_disposing (GError *error)
 {
 	if (error) {
 		if (error->domain == G_IO_ERROR)
 			return NM_IN_SET (error->code, G_IO_ERROR_CANCELLED);
-		if (consider_is_disposing) {
-			if (error->domain == NM_UTILS_ERROR)
-				return NM_IN_SET (error->code, NM_UTILS_ERROR_CANCELLED_DISPOSING);
-		}
+		if (error->domain == NM_UTILS_ERROR)
+			return NM_IN_SET (error->code, NM_UTILS_ERROR_CANCELLED_DISPOSING);
 	}
 	return FALSE;
 }
@@ -1913,18 +2427,20 @@ nm_g_type_find_implementing_class_for_property (GType gtype,
 /*****************************************************************************/
 
 static void
-_str_append_escape (GString *s, char ch)
+_str_buf_append_c_escape_octal (NMStrBuf *strbuf,
+                                char ch)
 {
-	g_string_append_c (s, '\\');
-	g_string_append_c (s, '0' + ((((guchar) ch) >> 6) & 07));
-	g_string_append_c (s, '0' + ((((guchar) ch) >> 3) & 07));
-	g_string_append_c (s, '0' + ( ((guchar) ch)       & 07));
+	nm_str_buf_append_c4 (strbuf,
+	                      '\\',
+	                      '0' + ((char) ((((guchar) ch) >> 6) & 07)),
+	                      '0' + ((char) ((((guchar) ch) >> 3) & 07)),
+	                      '0' + ((char) ((((guchar) ch)     ) & 07)));
 }
 
 gconstpointer
 nm_utils_buf_utf8safe_unescape (const char *str, gsize *out_len, gpointer *to_free)
 {
-	GString *gstr;
+	NMStrBuf strbuf;
 	gsize len;
 	const char *s;
 
@@ -1946,9 +2462,9 @@ nm_utils_buf_utf8safe_unescape (const char *str, gsize *out_len, gpointer *to_fr
 		return str;
 	}
 
-	gstr = g_string_new_len (NULL, len);
+	nm_str_buf_init (&strbuf, len, FALSE);
 
-	g_string_append_len (gstr, str, s - str);
+	nm_str_buf_append_len (&strbuf, str, s - str);
 	str = s;
 
 	for (;;) {
@@ -1971,6 +2487,9 @@ nm_utils_buf_utf8safe_unescape (const char *str, gsize *out_len, gpointer *to_fr
 				v = v * 8 + (ch - '0');
 				ch = (++str)[0];
 				if (ch >= '0' && ch <= '7') {
+					/* technically, escape sequences larger than \3FF are out of range
+					 * and invalid. We don't check for that, and do the same as
+					 * g_strcompress(): silently clip the value with & 0xFF. */
 					v = v * 8 + (ch - '0');
 					++str;
 				}
@@ -1992,21 +2511,20 @@ nm_utils_buf_utf8safe_unescape (const char *str, gsize *out_len, gpointer *to_fr
 			str++;
 		}
 
-		g_string_append_c (gstr, ch);
+		nm_str_buf_append_c (&strbuf, ch);
 
 		s = strchr (str, '\\');
 		if (!s) {
-			g_string_append (gstr, str);
+			nm_str_buf_append (&strbuf, str);
 			break;
 		}
 
-		g_string_append_len (gstr, str, s - str);
+		nm_str_buf_append_len (&strbuf, str, s - str);
 		str = s;
 	}
 
-	*out_len = gstr->len;
-	*to_free = gstr->str;
-	return g_string_free (gstr, FALSE);
+	return (*to_free = nm_str_buf_finalize (&strbuf,
+	                                        out_len));
 }
 
 /**
@@ -2047,7 +2565,7 @@ nm_utils_buf_utf8safe_escape (gconstpointer buf, gssize buflen, NMUtilsStrUtf8Sa
 	const char *p = NULL;
 	const char *s;
 	gboolean nul_terminated = FALSE;
-	GString *gstr;
+	NMStrBuf strbuf;
 
 	g_return_val_if_fail (to_free, NULL);
 
@@ -2078,7 +2596,9 @@ nm_utils_buf_utf8safe_escape (gconstpointer buf, gssize buflen, NMUtilsStrUtf8Sa
 			return str;
 	}
 
-	gstr = g_string_sized_new (buflen + 5);
+	nm_str_buf_init (&strbuf,
+	                 buflen + 5,
+	                 NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET));
 
 	s = str;
 	do {
@@ -2088,21 +2608,22 @@ nm_utils_buf_utf8safe_escape (gconstpointer buf, gssize buflen, NMUtilsStrUtf8Sa
 		for (; s < p; s++) {
 			char ch = s[0];
 
+			nm_assert (ch);
 			if (ch == '\\')
-				g_string_append (gstr, "\\\\");
+				nm_str_buf_append_c2 (&strbuf, '\\', '\\');
 			else if (   (   NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL) \
 			             && ch < ' ') \
 			         || (   NM_FLAGS_HAS (flags, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII) \
 			             && ((guchar) ch) >= 127))
-				_str_append_escape (gstr, ch);
+				_str_buf_append_c_escape_octal (&strbuf, ch);
 			else
-				g_string_append_c (gstr, ch);
+				nm_str_buf_append_c (&strbuf, ch);
 		}
 
 		if (buflen <= 0)
 			break;
 
-		_str_append_escape (gstr, p[0]);
+		_str_buf_append_c_escape_octal (&strbuf, p[0]);
 
 		buflen--;
 		if (buflen == 0)
@@ -2112,8 +2633,7 @@ nm_utils_buf_utf8safe_escape (gconstpointer buf, gssize buflen, NMUtilsStrUtf8Sa
 		(void) g_utf8_validate (s, buflen, &p);
 	} while (TRUE);
 
-	*to_free = g_string_free (gstr, FALSE);
-	return *to_free;
+	return (*to_free = nm_str_buf_finalize (&strbuf, NULL));
 }
 
 const char *
@@ -2137,13 +2657,11 @@ nm_utils_buf_utf8safe_escape_bytes (GBytes *bytes, NMUtilsStrUtf8SafeFlags flags
 const char *
 nm_utils_str_utf8safe_unescape (const char *str, char **to_free)
 {
+	gsize len;
+
 	g_return_val_if_fail (to_free, NULL);
 
-	if (!str || !strchr (str, '\\')) {
-		*to_free = NULL;
-		return str;
-	}
-	return (*to_free = g_strcompress (str));
+	return nm_utils_buf_utf8safe_unescape (str, &len, (gpointer *) to_free);
 }
 
 /**
@@ -2203,7 +2721,10 @@ nm_utils_str_utf8safe_escape_cp (const char *str, NMUtilsStrUtf8SafeFlags flags)
 char *
 nm_utils_str_utf8safe_unescape_cp (const char *str)
 {
-	return str ? g_strcompress (str) : NULL;
+	char *s;
+
+	str = nm_utils_str_utf8safe_unescape (str, &s);
+	return s ?: g_strdup (str);
 }
 
 char *
@@ -2224,7 +2745,7 @@ nm_utils_str_utf8safe_escape_take (char *str, NMUtilsStrUtf8SafeFlags flags)
 /* taken from systemd's fd_wait_for_event(). Note that the timeout
  * is here in nano-seconds, not micro-seconds. */
 int
-nm_utils_fd_wait_for_event (int fd, int event, gint64 timeout_ns)
+nm_utils_fd_wait_for_event (int fd, int event, gint64 timeout_nsec)
 {
 	struct pollfd pollfd = {
 		.fd = fd,
@@ -2233,11 +2754,11 @@ nm_utils_fd_wait_for_event (int fd, int event, gint64 timeout_ns)
 	struct timespec ts, *pts;
 	int r;
 
-	if (timeout_ns < 0)
+	if (timeout_nsec < 0)
 		pts = NULL;
 	else {
-		ts.tv_sec = (time_t) (timeout_ns / NM_UTILS_NS_PER_SECOND);
-		ts.tv_nsec = (long int) (timeout_ns % NM_UTILS_NS_PER_SECOND);
+		ts.tv_sec = (time_t) (timeout_nsec / NM_UTILS_NSEC_PER_SEC);
+		ts.tv_nsec = (long int) (timeout_nsec % NM_UTILS_NSEC_PER_SEC);
 		pts = &ts;
 	}
 
@@ -2319,7 +2840,10 @@ nm_utils_fd_read_loop_exact (int fd, void *buf, size_t nbytes, bool do_poll)
 /*****************************************************************************/
 
 NMUtilsNamedValue *
-nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len)
+nm_utils_named_values_from_str_dict_with_sort (GHashTable *hash,
+                                               guint *out_len,
+                                               GCompareDataFunc compare_func,
+                                               gpointer user_data)
 {
 	GHashTableIter iter;
 	NMUtilsNamedValue *values;
@@ -2342,7 +2866,8 @@ nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len)
 	values[i].name = NULL;
 	values[i].value_ptr = NULL;
 
-	nm_utils_named_value_list_sort (values, len, NULL, NULL);
+	if (compare_func)
+		nm_utils_named_value_list_sort (values, len, compare_func, user_data);
 
 	NM_SET_OUT (out_len, len);
 	return values;
@@ -3593,7 +4118,7 @@ nm_g_idle_source_new (int priority,
 }
 
 GSource *
-nm_g_timeout_source_new (guint timeout_ms,
+nm_g_timeout_source_new (guint timeout_msec,
                          int priority,
                          GSourceFunc func,
                          gpointer user_data,
@@ -3601,7 +4126,7 @@ nm_g_timeout_source_new (guint timeout_ms,
 {
 	GSource *source;
 
-	source = g_timeout_source_new (timeout_ms);
+	source = g_timeout_source_new (timeout_msec);
 	if (priority != G_PRIORITY_DEFAULT)
 		g_source_set_priority (source, priority);
 	g_source_set_callback (source, func, user_data, destroy_notify);
@@ -3625,6 +4150,26 @@ nm_g_unix_signal_source_new (int signum,
 	return source;
 }
 
+GSource *
+nm_g_unix_fd_source_new (int fd,
+                         GIOCondition io_condition,
+                         int priority,
+                         gboolean (*source_func) (int fd,
+                                                  GIOCondition condition,
+                                                  gpointer user_data),
+                         gpointer user_data,
+                         GDestroyNotify destroy_notify)
+{
+	GSource *source;
+
+	source = g_unix_fd_source_new (fd, io_condition);
+
+	if (priority != G_PRIORITY_DEFAULT)
+		g_source_set_priority (source, priority);
+	g_source_set_callback (source, G_SOURCE_FUNC (source_func), user_data, destroy_notify);
+	return source;
+}
+
 /*****************************************************************************/
 
 #define _CTX_LOG(fmt, ...) \
@@ -4038,8 +4583,47 @@ nm_utils_ifname_valid_kernel (const char *name, GError **error)
 	return FALSE;
 }
 
+/*****************************************************************************/
+
+static gboolean
+_nm_utils_ifname_valid_kernel (const char *name, GError **error)
+{
+	if (!nm_utils_ifname_valid_kernel (name, error))
+		return FALSE;
+
+	if (strchr (name, '%')) {
+		/* Kernel's dev_valid_name() accepts (almost) any binary up to 15 chars.
+		 * However, '%' is treated special as a format specifier. Try
+		 *
+		 *   ip link add 'dummy%dx' type dummy
+		 *
+		 * Don't allow that for "connection.interface-name", which either
+		 * matches an existing netdev name (thus, it cannot have a '%') or
+		 * is used to configure a name (in which case we don't want kernel
+		 * to replace the format specifier). */
+		g_set_error_literal (error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
+		                     _("'%%' is not allowed in interface names"));
+		return FALSE;
+	}
+
+	if (NM_IN_STRSET (name, "all",
+	                        "default",
+	                        "bonding_masters")) {
+		/* Certain names are not allowed. The "all" and "default" names are reserved
+		 * due to their directories in "/proc/sys/net/ipv4/conf/" and "/proc/sys/net/ipv6/conf/".
+		 *
+		 * Also, there is "/sys/class/net/bonding_masters" file.
+		 */
+		nm_utils_error_set (error, NM_UTILS_ERROR_UNKNOWN,
+		                    _("'%s' is not allowed as interface name"), name);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
 static gboolean
-_nm_utils_ifname_valid_ovs (const char* name, GError **error)
+_nm_utils_ifname_valid_ovs (const char *name, GError **error)
 {
 	const char *ch;
 
@@ -4081,10 +4665,90 @@ nm_utils_ifname_valid (const char* name,
 
 	switch (type) {
 	case NMU_IFACE_KERNEL:
-		return nm_utils_ifname_valid_kernel (name, error);
+		return _nm_utils_ifname_valid_kernel (name, error);
 	case NMU_IFACE_OVS:
 		return _nm_utils_ifname_valid_ovs (name, error);
+	case NMU_IFACE_OVS_AND_KERNEL:
+		return    _nm_utils_ifname_valid_kernel (name, error)
+		       && _nm_utils_ifname_valid_ovs (name, error);
+	case NMU_IFACE_ANY: {
+		gs_free_error GError *local = NULL;
+
+		if (_nm_utils_ifname_valid_kernel (name, error ? &local : NULL))
+			return TRUE;
+		if (_nm_utils_ifname_valid_ovs (name, NULL))
+			return TRUE;
+		if (error)
+			g_propagate_error (error, g_steal_pointer (&local));
+		return FALSE;
+	}
 	}
 
 	g_return_val_if_reached (FALSE);
 }
+
+/*****************************************************************************/
+
+void
+_nm_str_buf_ensure_size (NMStrBuf *strbuf,
+                         gsize new_size,
+                         gboolean reserve_exact)
+{
+	_nm_str_buf_assert (strbuf);
+
+	/* Currently this only supports strictly growing the buffer. */
+	nm_assert (new_size > strbuf->_priv_allocated);
+
+	if (!reserve_exact) {
+		new_size = nm_utils_get_next_realloc_size (!strbuf->_priv_do_bzero_mem,
+		                                           new_size);
+	}
+
+	strbuf->_priv_str = nm_secret_mem_realloc (strbuf->_priv_str,
+	                                           strbuf->_priv_do_bzero_mem,
+	                                           strbuf->_priv_allocated,
+	                                           new_size);
+	strbuf->_priv_allocated = new_size;
+}
+
+void
+nm_str_buf_append_printf (NMStrBuf *strbuf,
+                          const char *format,
+                          ...)
+{
+	va_list args;
+	gsize available;
+	int l;
+
+	_nm_str_buf_assert (strbuf);
+
+	available = strbuf->_priv_allocated - strbuf->_priv_len;
+
+	va_start (args, format);
+	l = g_vsnprintf (&strbuf->_priv_str[strbuf->_priv_len],
+	                 available,
+	                 format,
+	                 args);
+	va_end (args);
+
+	nm_assert (l >= 0);
+	nm_assert (l < G_MAXINT);
+
+	if ((gsize) l > available) {
+		gsize l2 = ((gsize) l) + 1u;
+
+		nm_str_buf_maybe_expand (strbuf, l2, FALSE);
+
+		va_start (args, format);
+		l = g_vsnprintf (&strbuf->_priv_str[strbuf->_priv_len],
+		                 l2,
+		                 format,
+		                 args);
+		va_end (args);
+
+		nm_assert (l >= 0);
+		nm_assert ((gsize) l == l2 - 1u);
+	}
+
+	strbuf->_priv_len += (gsize) l;
+}
diff --git a/shared/nm-glib-aux/nm-shared-utils.h b/shared/nm-glib-aux/nm-shared-utils.h
index 740e61d0..537b402b 100644
--- a/shared/nm-glib-aux/nm-shared-utils.h
+++ b/shared/nm-glib-aux/nm-shared-utils.h
@@ -115,11 +115,31 @@ typedef struct {
 		 * However, as ethernet addresses fit in here nicely, use
 		 * it also for an ethernet MAC address. */
 		guint8 addr_eth[6 /*ETH_ALEN*/];
+
+		guint8 array[sizeof (struct in6_addr)];
 	};
 } NMIPAddr;
 
+#define NM_IP_ADDR_INIT { .array = { 0 } }
+
 extern const NMIPAddr nm_ip_addr_zero;
 
+static inline int
+nm_ip_addr_cmp (int addr_family, gconstpointer a, gconstpointer b)
+{
+	nm_assert_addr_family (addr_family);
+	nm_assert (a);
+	nm_assert (b);
+
+	return memcmp (a, b, nm_utils_addr_family_to_size (addr_family));
+}
+
+static inline gboolean
+nm_ip_addr_equal (int addr_family, gconstpointer a, gconstpointer b)
+{
+	return nm_ip_addr_cmp (addr_family, a, b) == 0;
+}
+
 static inline gboolean
 nm_ip_addr_is_null (int addr_family, gconstpointer addr)
 {
@@ -158,6 +178,72 @@ nm_ip4_addr_is_localhost (in_addr_t addr4)
 
 /*****************************************************************************/
 
+#define NM_UTILS_INET_ADDRSTRLEN INET6_ADDRSTRLEN
+
+static inline const char *
+nm_utils_inet_ntop (int addr_family, gconstpointer addr, char *dst)
+{
+	const char *s;
+
+	const char *inet_ntop (int af,
+	                       const void *src,
+	                       char *dst,
+	                       socklen_t size);
+
+	nm_assert_addr_family (addr_family);
+	nm_assert (addr);
+	nm_assert (dst);
+
+	s = inet_ntop (addr_family,
+	               addr,
+	               dst,
+	               addr_family == AF_INET6 ? INET6_ADDRSTRLEN : INET_ADDRSTRLEN);
+	nm_assert (s);
+	return s;
+}
+
+static inline const char *
+_nm_utils_inet4_ntop (in_addr_t addr, char dst[static INET_ADDRSTRLEN])
+{
+	return nm_utils_inet_ntop (AF_INET, &addr, dst);
+}
+
+static inline const char *
+_nm_utils_inet6_ntop (const struct in6_addr *addr, char dst[static INET6_ADDRSTRLEN])
+{
+	return nm_utils_inet_ntop (AF_INET6, addr, dst);
+}
+
+static inline char *
+nm_utils_inet_ntop_dup (int addr_family, gconstpointer addr)
+{
+	char buf[NM_UTILS_INET_ADDRSTRLEN];
+
+	return g_strdup (nm_utils_inet_ntop (addr_family, addr, buf));
+}
+
+static inline char *
+nm_utils_inet4_ntop_dup (in_addr_t addr)
+{
+	return nm_utils_inet_ntop_dup (AF_INET, &addr);
+}
+
+static inline char *
+nm_utils_inet6_ntop_dup (const struct in6_addr *addr)
+{
+	return nm_utils_inet_ntop_dup (AF_INET6, addr);
+}
+
+/*****************************************************************************/
+
+gboolean nm_utils_ipaddr_is_valid (int addr_family,
+                                   const char *str_addr);
+
+gboolean nm_utils_ipaddr_is_normalized (int addr_family,
+                                        const char *str_addr);
+
+/*****************************************************************************/
+
 #define NM_CMP_RETURN(c) \
     G_STMT_START { \
         const int _cc = (c); \
@@ -307,6 +393,8 @@ gboolean nm_utils_gbytes_equal_mem (GBytes *bytes,
 
 GVariant *nm_utils_gbytes_to_variant_ay (GBytes *bytes);
 
+GVariant *nm_utils_strdict_to_variant_ass (GHashTable *strdict);
+
 /*****************************************************************************/
 
 GVariant *nm_utils_gvariant_vardict_filter (GVariant *src,
@@ -352,7 +440,25 @@ int nm_utils_dbus_path_cmp (const char *dbus_path_a, const char *dbus_path_b);
 
 typedef enum {
 	NM_UTILS_STRSPLIT_SET_FLAGS_NONE           = 0,
+
+	/* by default, strsplit will coalesce consecutive delimiters and remove
+	 * them from the result. If this flag is present, empty values are preserved
+	 * and returned.
+	 *
+	 * When combined with %NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP, if a value gets
+	 * empty after strstrip(), it also gets removed. */
 	NM_UTILS_STRSPLIT_SET_FLAGS_PRESERVE_EMPTY = (1u << 0),
+
+	/* %NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING means that delimiters prefixed
+	 * by a backslash are not treated as a separator. Such delimiters and their escape
+	 * character are copied to the current word without unescaping them. In general,
+	 * nm_utils_strsplit_set_full() does not remove any backslash escape characters
+	 * and does no unescaping. It only considers them for skipping to split at
+	 * an escaped delimiter.
+	 *
+	 * If this is combined with (or implied by %NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED), then
+	 * the backslash escapes are removed from the result.
+	 */
 	NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING = (1u << 1),
 
 	/* If flag is set, does the same as g_strstrip() on the returned tokens.
@@ -392,6 +498,7 @@ typedef enum {
 	 * need extra care, and then only if they proceed one of the relevant characters.
 	 */
 	NM_UTILS_STRSPLIT_SET_FLAGS_ESCAPED        = (1u << 3),
+
 } NMUtilsStrsplitSetFlags;
 
 const char **nm_utils_strsplit_set_full (const char *str,
@@ -435,9 +542,65 @@ nm_utils_escaped_tokens_split (const char *str,
 	                                   | NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
 }
 
-const char *nm_utils_escaped_tokens_escape (const char *str,
-                                            const char *delimiters,
-                                            char **out_to_free);
+typedef enum {
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_NONE                       = 0,
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_SPACES              = (1ull << 0),
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE       = (1ull << 1),
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE      = (1ull << 2),
+
+	/* Backslash characters will be escaped as "\\\\" if they precede another
+	 * character that makes it necessary. Such characters are:
+	 *
+	 *  1) before another '\\' backslash.
+	 *  2) before any delimiter in @delimiters.
+	 *  3) before any delimiter in @delimiters_as_needed.
+	 *  4) before a white space, if ESCAPE_LEADING_SPACE or ESCAPE_TRAILING_SPACE is set.
+	 *  5) before the end of the word
+	 *
+	 * Rule 4) is an extension. It's not immediately clear why with ESCAPE_LEADING_SPACE
+	 * and ESCAPE_TRAILING_SPACE we want *all* backslashes before a white space escaped.
+	 * The reason is, that we obviously want to use ESCAPE_LEADING_SPACE and ESCAPE_TRAILING_SPACE
+	 * in cases, where we later parse the backslash escaped strings back, but allowing to strip
+	 * unescaped white spaces. That means, we want that " a " gets escaped as "\\ a\\ ".
+	 * On the other hand, we also want that " a\\ b " gets escaped as "\\ a\\\\ b\\ ",
+	 * and not "\\ a\\ b\\ ". Because otherwise, the parser would need to treat "\\ "
+	 * differently depending on whether the sequence is at the beginning, end or middle
+	 * of the word.
+	 *
+	 * Rule 5) is also not immediately obvious. When used with ESCAPE_TRAILING_SPACE,
+	 * we clearly want to allow that an escaped word can have arbitrary
+	 * whitespace suffixes. That's why this mode exists. So we must escape "a\\" as
+	 * "a\\\\", so that appending " " does not change the meaning.
+	 * Also without ESCAPE_TRAILING_SPACE, we want in general that we can concatenate
+	 * two escaped words without changing their meaning. If the words would be "a\\"
+	 * and "," (with ',' being a delimiter), then the result must be "a\\\\" and "\\,"
+	 * so that the concatenated word ("a\\\\\\,") is still the same. If we would escape
+	 * them instead as "a\\" + "\\,", then the concatenated word would be "a\\\\," and
+	 * different.
+	 * */
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED = (1ull << 3),
+
+	NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS    = (1ull << 4),
+} NMUtilsEscapedTokensEscapeFlags;
+
+const char *nm_utils_escaped_tokens_escape_full (const char *str,
+                                                 const char *delimiters,
+                                                 const char *delimiters_as_needed,
+                                                 NMUtilsEscapedTokensEscapeFlags flags,
+                                                 char **out_to_free);
+
+static inline const char *
+nm_utils_escaped_tokens_escape (const char *str,
+                                const char *delimiters,
+                                char **out_to_free)
+{
+	return nm_utils_escaped_tokens_escape_full (str,
+	                                            delimiters,
+	                                            NULL,
+	                                              NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_ALWAYS
+	                                            | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE,
+	                                            out_to_free);
+}
 
 static inline GString *
 nm_utils_escaped_tokens_escape_gstr_assert (const char *str,
@@ -489,6 +652,47 @@ nm_utils_escaped_tokens_escape_gstr (const char *str,
 
 /*****************************************************************************/
 
+static inline const char **
+nm_utils_escaped_tokens_options_split_list (const char *str)
+{
+	return nm_utils_strsplit_set_full (str,
+	                                   ",",
+	                                     NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP
+	                                   | NM_UTILS_STRSPLIT_SET_FLAGS_ALLOW_ESCAPING);
+}
+
+void nm_utils_escaped_tokens_options_split (char *str,
+                                            const char **out_key,
+                                            const char **out_val);
+
+static inline const char *
+nm_utils_escaped_tokens_options_escape_key (const char *key,
+                                            char **out_to_free)
+{
+	return nm_utils_escaped_tokens_escape_full (key,
+	                                            ",=",
+	                                            NULL,
+	                                              NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED
+	                                            | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
+	                                            | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE,
+	                                            out_to_free);
+}
+
+static inline const char *
+nm_utils_escaped_tokens_options_escape_val (const char *val,
+                                            char **out_to_free)
+{
+	return nm_utils_escaped_tokens_escape_full (val,
+	                                            ",",
+	                                            "=",
+	                                              NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_BACKSLASH_AS_NEEDED
+	                                            | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_LEADING_SPACE
+	                                            | NM_UTILS_ESCAPED_TOKENS_ESCAPE_FLAGS_ESCAPE_TRAILING_SPACE,
+	                                            out_to_free);
+}
+
+/*****************************************************************************/
+
 #define NM_UTILS_CHECKSUM_LENGTH_MD5          16
 #define NM_UTILS_CHECKSUM_LENGTH_SHA1         20
 #define NM_UTILS_CHECKSUM_LENGTH_SHA256       32
@@ -571,6 +775,17 @@ gboolean nm_utils_parse_inaddr_prefix (int addr_family,
                                        char **out_addr,
                                        int *out_prefix);
 
+gint64 nm_g_ascii_strtoll (const char *nptr,
+                           char **endptr,
+                           guint base);
+
+guint64 nm_g_ascii_strtoull (const char *nptr,
+                             char **endptr,
+                             guint base);
+
+double nm_g_ascii_strtod (const char *nptr,
+                          char **endptr);
+
 gint64  _nm_utils_ascii_str_to_int64  (const char *str, guint base, gint64  min, gint64  max, gint64  fallback);
 guint64 _nm_utils_ascii_str_to_uint64 (const char *str, guint base, guint64 min, guint64 max, guint64 fallback);
 
@@ -593,8 +808,8 @@ typedef struct {
 
 #define NM_UTILS_FLAGS2STR(f, n) { .flag = f, .name = ""n, }
 
-#define _NM_UTILS_FLAGS2STR_DEFINE(scope, fcn_name, flags_type, ...) \
-scope const char * \
+#define NM_UTILS_FLAGS2STR_DEFINE(fcn_name, flags_type, ...) \
+const char * \
 fcn_name (flags_type flags, char *buf, gsize len) \
 { \
 	static const NMUtilsFlags2StrDesc descs[] = { \
@@ -604,11 +819,6 @@ fcn_name (flags_type flags, char *buf, gsize len) \
 	return nm_utils_flags2str (descs, G_N_ELEMENTS (descs), flags, buf, len); \
 };
 
-#define NM_UTILS_FLAGS2STR_DEFINE(fcn_name, flags_type, ...) \
-	_NM_UTILS_FLAGS2STR_DEFINE (, fcn_name, flags_type, __VA_ARGS__)
-#define NM_UTILS_FLAGS2STR_DEFINE_STATIC(fcn_name, flags_type, ...) \
-	_NM_UTILS_FLAGS2STR_DEFINE (static, fcn_name, flags_type, __VA_ARGS__)
-
 const char *nm_utils_flags2str (const NMUtilsFlags2StrDesc *descs,
                                 gsize n_descs,
                                 unsigned flags,
@@ -620,8 +830,8 @@ const char *nm_utils_flags2str (const NMUtilsFlags2StrDesc *descs,
 #define NM_UTILS_ENUM2STR(v, n)     (void) 0; case v: s = ""n""; break; (void) 0
 #define NM_UTILS_ENUM2STR_IGNORE(v) (void) 0; case v: break; (void) 0
 
-#define _NM_UTILS_ENUM2STR_DEFINE(scope, fcn_name, lookup_type, int_fmt, ...) \
-scope const char * \
+#define NM_UTILS_ENUM2STR_DEFINE_FULL(fcn_name, lookup_type, int_fmt, ...) \
+const char * \
 fcn_name (lookup_type val, char *buf, gsize len) \
 { \
 	nm_utils_to_string_buffer_init (&buf, &len); \
@@ -641,9 +851,7 @@ fcn_name (lookup_type val, char *buf, gsize len) \
 }
 
 #define NM_UTILS_ENUM2STR_DEFINE(fcn_name, lookup_type, ...) \
-	_NM_UTILS_ENUM2STR_DEFINE (, fcn_name, lookup_type, "d", __VA_ARGS__)
-#define NM_UTILS_ENUM2STR_DEFINE_STATIC(fcn_name, lookup_type, ...) \
-	_NM_UTILS_ENUM2STR_DEFINE (static, fcn_name, lookup_type, "d", __VA_ARGS__)
+	NM_UTILS_ENUM2STR_DEFINE_FULL (fcn_name, lookup_type, "d", __VA_ARGS__)
 
 /*****************************************************************************/
 
@@ -797,8 +1005,15 @@ nm_utils_error_new_cancelled (gboolean is_disposing,
 	return error;
 }
 
-gboolean nm_utils_error_is_cancelled (GError *error,
-                                      gboolean consider_is_disposing);
+gboolean nm_utils_error_is_cancelled_or_disposing (GError *error);
+
+static inline gboolean
+nm_utils_error_is_cancelled (GError *error)
+{
+	return    error
+	       && error->code == G_IO_ERROR_CANCELLED
+	       && error->domain == G_IO_ERROR;
+}
 
 gboolean nm_utils_error_is_notfound (GError *error);
 
@@ -927,6 +1142,7 @@ typedef enum {
 	NM_UTILS_STR_UTF8_SAFE_FLAG_NONE                = 0,
 	NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL         = 0x0001,
 	NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII    = 0x0002,
+	NM_UTILS_STR_UTF8_SAFE_FLAG_SECRET              = 0x0004,
 } NMUtilsStrUtf8SafeFlags;
 
 const char *nm_utils_buf_utf8safe_escape (gconstpointer buf, gssize buflen, NMUtilsStrUtf8SafeFlags flags, char **to_free);
@@ -958,6 +1174,32 @@ nm_g_variant_unref_floating (GVariant *var)
 		g_variant_unref (var);
 }
 
+#define nm_g_variant_lookup(dictionary, ...) \
+	({ \
+		GVariant *const _dictionary = (dictionary); \
+		\
+		(   _dictionary \
+		 && g_variant_lookup (_dictionary, __VA_ARGS__)); \
+	})
+
+static inline GVariant *
+nm_g_variant_lookup_value (GVariant *dictionary,
+                           const char *key,
+                           const GVariantType *expected_type)
+{
+	return   dictionary
+	       ? g_variant_lookup_value (dictionary, key, expected_type)
+	       : NULL;
+}
+
+static inline gboolean
+nm_g_variant_is_of_type (GVariant *value,
+                         const GVariantType *type)
+{
+	return    value
+	       && g_variant_is_of_type (value, type);
+}
+
 static inline void
 nm_g_source_destroy_and_unref (GSource *source)
 {
@@ -986,12 +1228,19 @@ GSource *nm_g_idle_source_new (int priority,
                                gpointer user_data,
                                GDestroyNotify destroy_notify);
 
-GSource *nm_g_timeout_source_new (guint timeout_ms,
+GSource *nm_g_timeout_source_new (guint timeout_msec,
                                   int priority,
                                   GSourceFunc func,
                                   gpointer user_data,
                                   GDestroyNotify destroy_notify);
-
+GSource *nm_g_unix_fd_source_new (int fd,
+                                  GIOCondition io_condition,
+                                  int priority,
+                                  gboolean (*source_func) (int fd,
+                                                           GIOCondition condition,
+                                                           gpointer user_data),
+                                  gpointer user_data,
+                                  GDestroyNotify destroy_notify);
 GSource *nm_g_unix_signal_source_new (int signum,
                                       int priority,
                                       GSourceFunc handler,
@@ -1019,6 +1268,25 @@ nm_g_main_context_push_thread_default (GMainContext *context)
 	return context;
 }
 
+static inline gboolean
+nm_g_main_context_is_thread_default (GMainContext *context)
+{
+	GMainContext *cur_context;
+
+	cur_context = g_main_context_get_thread_default ();
+	if (cur_context == context)
+		return TRUE;
+
+	if (G_UNLIKELY (!cur_context))
+		cur_context = g_main_context_default ();
+	else if (G_UNLIKELY (!context))
+		context = g_main_context_default ();
+	else
+		return FALSE;
+
+	return (cur_context == context);
+}
+
 static inline GMainContext *
 nm_g_main_context_push_thread_default_if_necessary (GMainContext *context)
 {
@@ -1077,7 +1345,18 @@ typedef struct {
 	};
 } NMUtilsNamedValue;
 
-NMUtilsNamedValue *nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len);
+NMUtilsNamedValue *nm_utils_named_values_from_str_dict_with_sort (GHashTable *hash,
+                                                                  guint *out_len,
+                                                                  GCompareDataFunc compare_func,
+                                                                  gpointer user_data);
+
+static inline NMUtilsNamedValue *
+nm_utils_named_values_from_str_dict (GHashTable *hash, guint *out_len)
+{
+	G_STATIC_ASSERT (G_STRUCT_OFFSET (NMUtilsNamedValue, name) == 0);
+
+	return nm_utils_named_values_from_str_dict_with_sort (hash, out_len, nm_strcmp_p_with_data, NULL);
+}
 
 gssize nm_utils_named_value_list_find (const NMUtilsNamedValue *arr,
                                        gsize len,
@@ -1146,6 +1425,26 @@ char *nm_utils_g_slist_strlist_join (const GSList *a, const char *separator);
 
 /*****************************************************************************/
 
+static inline guint
+nm_g_hash_table_size (GHashTable *hash)
+{
+	return hash ? g_hash_table_size (hash) : 0u;
+}
+
+static inline gpointer
+nm_g_hash_table_lookup (GHashTable *hash, gconstpointer key)
+{
+	return hash ? g_hash_table_lookup (hash, key) : NULL;
+}
+
+static inline gboolean
+nm_g_hash_table_remove (GHashTable *hash, gconstpointer key)
+{
+	return hash ? g_hash_table_remove (hash, key) : FALSE;
+}
+
+/*****************************************************************************/
+
 gssize nm_utils_ptrarray_find_binary_search (gconstpointer *list,
                                              gsize len,
                                              gconstpointer needle,
@@ -1190,31 +1489,25 @@ _nm_utils_strv_equal (char **strv1, char **strv2)
 
 /*****************************************************************************/
 
-#define NM_UTILS_NS_PER_SECOND   ((gint64) 1000000000)
-#define NM_UTILS_NS_PER_MSEC     ((gint64) 1000000)
-#define NM_UTILS_MSEC_PER_SECOND ((gint64) 1000)
-#define NM_UTILS_NS_TO_MSEC_CEIL(nsec)      (((nsec) + (NM_UTILS_NS_PER_MSEC - 1)) / NM_UTILS_NS_PER_MSEC)
+#define NM_UTILS_NSEC_PER_SEC  ((gint64) 1000000000)
+#define NM_UTILS_USEC_PER_SEC  ((gint64) 1000000)
+#define NM_UTILS_MSEC_PER_SEC  ((gint64) 1000)
+#define NM_UTILS_NSEC_PER_MSEC ((gint64) 1000000)
+
+static inline gint64
+NM_UTILS_NSEC_TO_MSEC_CEIL (gint64 nsec)
+{
+	return (nsec + (NM_UTILS_NSEC_PER_MSEC - 1)) / NM_UTILS_NSEC_PER_MSEC;
+}
 
 /*****************************************************************************/
 
-int nm_utils_fd_wait_for_event (int fd, int event, gint64 timeout_ns);
+int nm_utils_fd_wait_for_event (int fd, int event, gint64 timeout_nsec);
 ssize_t nm_utils_fd_read_loop (int fd, void *buf, size_t nbytes, bool do_poll);
 int nm_utils_fd_read_loop_exact (int fd, void *buf, size_t nbytes, bool do_poll);
 
 /*****************************************************************************/
 
-static inline const char *
-nm_utils_dbus_normalize_object_path (const char *path)
-{
-	/* D-Bus does not allow an empty object path. Hence, whenever we mean NULL / no-object
-	 * on D-Bus, it's path is actually "/".
-	 *
-	 * Normalize that away, and return %NULL in that case. */
-	if (path && path[0] == '/' && path[1] == '\0')
-		return NULL;
-	return path;
-}
-
 #define NM_DEFINE_GDBUS_ARG_INFO_FULL(name_, ...) \
 	((GDBusArgInfo *) (&((const GDBusArgInfo) { \
 		.ref_count = -1, \
@@ -1412,6 +1705,92 @@ guint8 *nm_utils_hexstr2bin_alloc (const char *hexstr,
 
 /*****************************************************************************/
 
+#define _NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(fcn_name, \
+                                             value_type, \
+                                             value_type_result, \
+                                             entry_cmd, \
+                                             unknown_val_cmd, \
+                                             get_operator, \
+                                             ...) \
+value_type_result \
+fcn_name (const char *name) \
+{ \
+	static const struct { \
+		const char *name; \
+		value_type value; \
+	} LIST[] = { \
+		__VA_ARGS__ \
+	}; \
+	\
+	if (NM_MORE_ASSERT_ONCE (5)) { \
+		int i; \
+		\
+		for (i = 0; i < G_N_ELEMENTS (LIST); i++) { \
+			nm_assert (LIST[i].name); \
+			if (i > 0) \
+				nm_assert (strcmp (LIST[i - 1].name, LIST[i].name) < 0); \
+		} \
+	} \
+	\
+	{ entry_cmd; } \
+	\
+	if (G_LIKELY (name)) { \
+		G_STATIC_ASSERT (G_N_ELEMENTS (LIST) > 1); \
+		G_STATIC_ASSERT (G_N_ELEMENTS (LIST) < G_MAXINT / 2 - 10); \
+		int imin = 0; \
+		int imax = (G_N_ELEMENTS (LIST) - 1); \
+		int imid = (G_N_ELEMENTS (LIST) - 1) / 2; \
+		\
+		for (;;) { \
+			const int cmp = strcmp (LIST[imid].name, name); \
+			\
+			if (G_UNLIKELY (cmp == 0)) \
+				return get_operator (LIST[imid].value); \
+			\
+			if (cmp < 0) \
+				imin = imid + 1; \
+			else \
+				imax = imid - 1; \
+			\
+			if (G_UNLIKELY (imin > imax)) \
+				break; \
+			\
+			/* integer overflow cannot happen, because LIST is shorter than G_MAXINT/2. */ \
+			imid = (imin + imax) / 2;\
+		} \
+	} \
+	\
+	{ unknown_val_cmd; } \
+}
+
+#define NM_UTILS_STRING_TABLE_LOOKUP_STRUCT_DEFINE(fcn_name, \
+                                                   result_type, \
+                                                   entry_cmd, \
+                                                   unknown_val_cmd, \
+                                                   ...) \
+	_NM_UTILS_STRING_TABLE_LOOKUP_DEFINE (fcn_name, \
+	                                      result_type, \
+	                                      const result_type *, \
+	                                      entry_cmd, \
+	                                      unknown_val_cmd, \
+	                                      &, \
+	                                      __VA_ARGS__)
+
+#define NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(fcn_name, \
+                                            result_type, \
+                                            entry_cmd, \
+                                            unknown_val_cmd, \
+                                            ...) \
+	_NM_UTILS_STRING_TABLE_LOOKUP_DEFINE (fcn_name, \
+	                                      result_type, \
+	                                      result_type, \
+	                                      entry_cmd, \
+	                                      unknown_val_cmd, \
+	                                      , \
+	                                      __VA_ARGS__)
+
+/*****************************************************************************/
+
 static inline GTask *
 nm_g_task_new (gpointer source_object,
                GCancellable *cancellable,
@@ -1442,9 +1821,49 @@ guint nm_utils_parse_debug_string (const char *string,
 
 /*****************************************************************************/
 
+static inline gboolean
+nm_utils_strdup_reset (char **dst, const char *src)
+{
+	nm_assert (dst);
+
+	if (nm_streq0 (*dst, src))
+		return FALSE;
+	g_free (*dst);
+	*dst = g_strdup (src);
+	return TRUE;
+}
+
+/*****************************************************************************/
+
+/* nm_utils_get_next_realloc_size() is used to grow buffers exponentially, when
+ * the final size is unknown. As such, it has borders for which it allocates
+ * certain buffer sizes.
+ *
+ * The use of these defines is to get favorable allocation sequences.
+ * For example, nm_str_buf_init() asks for an initial allocation size. Note that
+ * it reserves the exactly requested amount, under the assumption that the
+ * user may know how many bytes will be required. However, often the caller
+ * doesn't know in advance, and NMStrBuf grows exponentially by calling
+ * nm_utils_get_next_realloc_size().
+ * Imagine you call nm_str_buf_init() with an initial buffer size 100, and you
+ * add one character at a time. Then the first reallocation will increase the
+ * buffer size only from 100 to 104.
+ * If you however start with an initial buffer size of 104, then the next reallocation
+ * via nm_utils_get_next_realloc_size() gives you 232, and so on. By using
+ * these sizes, it results in one less allocation, if you anyway don't know the
+ * exact size in advance. */
+#define NM_UTILS_GET_NEXT_REALLOC_SIZE_104     ((gsize) 104)
+#define NM_UTILS_GET_NEXT_REALLOC_SIZE_1000    ((gsize) 1000)
+
+gsize nm_utils_get_next_realloc_size (gboolean true_realloc, gsize requested);
+
+/*****************************************************************************/
+
 typedef enum {
-	NMU_IFACE_KERNEL = 0,
+	NMU_IFACE_ANY,
+	NMU_IFACE_KERNEL,
 	NMU_IFACE_OVS,
+	NMU_IFACE_OVS_AND_KERNEL,
 } NMUtilsIfaceType;
 
 gboolean nm_utils_ifname_valid_kernel (const char *name, GError **error);
diff --git a/shared/nm-glib-aux/nm-str-buf.h b/shared/nm-glib-aux/nm-str-buf.h
new file mode 100644
index 00000000..8c73bfaf
--- /dev/null
+++ b/shared/nm-glib-aux/nm-str-buf.h
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NM_STR_BUF_H__
+#define __NM_STR_BUF_H__
+
+#include "nm-shared-utils.h"
+#include "nm-secret-utils.h"
+
+/*****************************************************************************/
+
+/* NMStrBuf is not unlike GString. The main difference is that it can use
+ * nm_explicit_bzero() when growing the buffer. */
+typedef struct _NMStrBuf {
+
+	char *_priv_str;
+
+	/* The unions only exist because we allow/encourage read-only access
+	 * to the "len" and "allocated" fields, but modifying the fields is
+	 * only allowed to the NMStrBuf implementation itself. */
+	union {
+		/*const*/ gsize len;
+		gsize _priv_len;
+	};
+	union {
+		/*const*/ gsize allocated;
+		gsize _priv_allocated;
+	};
+
+	bool _priv_do_bzero_mem;
+} NMStrBuf;
+
+/*****************************************************************************/
+
+static inline void
+_nm_str_buf_assert (NMStrBuf *strbuf)
+{
+	nm_assert (strbuf);
+	nm_assert (strbuf->_priv_str);
+	nm_assert (strbuf->_priv_allocated > 0);
+	nm_assert (strbuf->_priv_len <= strbuf->_priv_allocated);
+}
+
+static inline void
+nm_str_buf_init (NMStrBuf *strbuf,
+                 gsize len,
+                 bool do_bzero_mem)
+{
+	nm_assert (strbuf);
+	nm_assert (len > 0);
+
+	strbuf->_priv_str          = g_malloc (len);
+	strbuf->_priv_allocated    = len;
+	strbuf->_priv_len          = 0;
+	strbuf->_priv_do_bzero_mem = do_bzero_mem;
+
+	_nm_str_buf_assert (strbuf);
+}
+
+void _nm_str_buf_ensure_size (NMStrBuf *strbuf,
+                              gsize new_size,
+                              gboolean reserve_exact);
+
+static inline void
+nm_str_buf_maybe_expand (NMStrBuf *strbuf,
+                         gsize reserve,
+                         gboolean reserve_exact)
+{
+	_nm_str_buf_assert (strbuf);
+
+	/* currently we always require to reserve a non-zero number of bytes. */
+	nm_assert (reserve > 0);
+	nm_assert (strbuf->_priv_len < G_MAXSIZE - reserve);
+
+	/* @reserve is the extra space that we require. */
+	if (G_UNLIKELY (reserve > strbuf->_priv_allocated - strbuf->_priv_len))
+		_nm_str_buf_ensure_size (strbuf, strbuf->_priv_len + reserve, reserve_exact);
+}
+
+/*****************************************************************************/
+
+/**
+ * nm_str_buf_set_size:
+ * @strbuf: the initialized #NMStrBuf
+ * @new_len: the new length
+ * @honor_do_bzero_mem: if %TRUE, the shrinked memory will be cleared, if
+ *   do_bzero_mem is set. This should be usually set to %TRUE, unless
+ *   you know that the shrinked memory does not contain data that requires to be
+ *   cleared. When growing the size, this value has no effect.
+ * @reserve_exact: when growing the buffer, reserve the exact amount of bytes.
+ *   If %FALSE, the buffer may allocate more memory than requested to grow
+ *   exponentially.
+ *
+ * This is like g_string_set_size(). If new_len is smaller than the
+ * current length, the string gets truncated (excess memory will be cleared).
+ *
+ * When extending the length, the added bytes are undefined (like with
+ * g_string_set_size(). Likewise, if you first pre-allocate a buffer with
+ * nm_str_buf_maybe_expand(), then write to the bytes, and finally set
+ * the appropriate size, then that works as expected (by not clearing the
+ * pre-existing, grown buffer).
+ */
+static inline void
+nm_str_buf_set_size (NMStrBuf *strbuf,
+                     gsize new_len,
+                     gboolean honor_do_bzero_mem,
+                     gboolean reserve_exact)
+{
+	_nm_str_buf_assert (strbuf);
+
+	if (new_len < strbuf->_priv_len) {
+		if (   honor_do_bzero_mem
+		    && strbuf->_priv_do_bzero_mem) {
+			/* we only clear the memory that we wrote to. */
+			nm_explicit_bzero (&strbuf->_priv_str[new_len], strbuf->_priv_len - new_len);
+		}
+	} else if (new_len > strbuf->_priv_len) {
+		nm_str_buf_maybe_expand (strbuf,
+		                         new_len - strbuf->_priv_len + (reserve_exact ? 0u : 1u),
+		                         reserve_exact);
+	} else
+		return;
+
+	strbuf->_priv_len = new_len;
+}
+
+/*****************************************************************************/
+
+static inline void
+nm_str_buf_erase (NMStrBuf *strbuf,
+                  gsize pos,
+                  gssize len,
+                  gboolean honor_do_bzero_mem)
+{
+	gsize new_len;
+
+	_nm_str_buf_assert (strbuf);
+
+	nm_assert (pos <= strbuf->_priv_len);
+
+	if (len == 0)
+		return;
+
+	if (len < 0) {
+		/* truncate the string before pos */
+		nm_assert (len == -1);
+		new_len = pos;
+	} else {
+		gsize l = len;
+
+		nm_assert (l <= strbuf->_priv_len - pos);
+
+		new_len = strbuf->_priv_len - l;
+		if (pos + l < strbuf->_priv_len) {
+			memmove (&strbuf->_priv_str[pos],
+			         &strbuf->_priv_str[pos + l],
+			         strbuf->_priv_len - (pos + l));
+		}
+	}
+
+	nm_assert (new_len <= strbuf->_priv_len);
+	nm_str_buf_set_size (strbuf, new_len, honor_do_bzero_mem, TRUE);
+}
+
+/*****************************************************************************/
+
+static inline void
+nm_str_buf_append_c (NMStrBuf *strbuf,
+                     char ch)
+{
+	nm_str_buf_maybe_expand (strbuf, 2, FALSE);
+	strbuf->_priv_str[strbuf->_priv_len++] = ch;
+}
+
+static inline void
+nm_str_buf_append_c2 (NMStrBuf *strbuf,
+                      char ch0,
+                      char ch1)
+{
+	nm_str_buf_maybe_expand (strbuf, 3, FALSE);
+	strbuf->_priv_str[strbuf->_priv_len++] = ch0;
+	strbuf->_priv_str[strbuf->_priv_len++] = ch1;
+}
+
+static inline void
+nm_str_buf_append_c4 (NMStrBuf *strbuf,
+                      char ch0,
+                      char ch1,
+                      char ch2,
+                      char ch3)
+{
+	nm_str_buf_maybe_expand (strbuf, 5, FALSE);
+	strbuf->_priv_str[strbuf->_priv_len++] = ch0;
+	strbuf->_priv_str[strbuf->_priv_len++] = ch1;
+	strbuf->_priv_str[strbuf->_priv_len++] = ch2;
+	strbuf->_priv_str[strbuf->_priv_len++] = ch3;
+}
+
+static inline void
+nm_str_buf_append_len (NMStrBuf *strbuf,
+                       const char *str,
+                       gsize len)
+{
+	_nm_str_buf_assert (strbuf);
+
+	if (len > 0) {
+		nm_str_buf_maybe_expand (strbuf, len + 1, FALSE);
+		memcpy (&strbuf->_priv_str[strbuf->_priv_len], str, len);
+		strbuf->_priv_len += len;
+	}
+}
+
+static inline void
+nm_str_buf_append (NMStrBuf *strbuf,
+                   const char *str)
+{
+	nm_assert (str);
+
+	nm_str_buf_append_len (strbuf, str, strlen (str));
+}
+
+void nm_str_buf_append_printf (NMStrBuf *strbuf,
+                               const char *format,
+                               ...) _nm_printf (2, 3);
+
+static inline void
+nm_str_buf_ensure_trailing_c (NMStrBuf *strbuf, char ch)
+{
+	_nm_str_buf_assert (strbuf);
+
+	if (   strbuf->_priv_len == 0
+	    || strbuf->_priv_str[strbuf->_priv_len - 1] != ch)
+		nm_str_buf_append_c (strbuf, ch);
+}
+
+/*****************************************************************************/
+
+static inline gboolean
+nm_str_buf_is_initalized (NMStrBuf *strbuf)
+{
+	nm_assert (strbuf);
+#if NM_MORE_ASSERTS
+	if (strbuf->_priv_str)
+		_nm_str_buf_assert (strbuf);
+#endif
+	return !!strbuf->_priv_str;
+}
+
+/**
+ * nm_str_buf_get_str:
+ * @strbuf: the #NMStrBuf instance
+ *
+ * Returns the NUL terminated internal string.
+ *
+ * While constructing the string, the intermediate buffer
+ * is not NUL terminated (this makes it different from GString).
+ * Usually, one would build the string and retrieve it at the
+ * end with nm_str_buf_finalize(). This returns the NUL terminated
+ * buffer that was appended so far. Contrary to nm_str_buf_finalize(), you
+ * can still append more data to the buffer and this does not transfer ownership
+ * of the string.
+ *
+ * Returns: (transfer none): the internal string. The string
+ *   is of length "strbuf->len", which may be larger if the
+ *   returned string contains NUL characters (binary). The terminating
+ *   NUL character is always present after "strbuf->len" characters.
+ */
+static inline const char *
+nm_str_buf_get_str (NMStrBuf *strbuf)
+{
+	nm_str_buf_maybe_expand (strbuf, 1, FALSE);
+	strbuf->_priv_str[strbuf->_priv_len] = '\0';
+	return strbuf->_priv_str;
+}
+
+static inline char *
+nm_str_buf_get_str_unsafe (NMStrBuf *strbuf)
+{
+	_nm_str_buf_assert (strbuf);
+	return strbuf->_priv_str;
+}
+
+/**
+ * nm_str_buf_finalize:
+ * @strbuf: an initilized #NMStrBuf
+ * @out_len: (out): (allow-none): optional output
+ *   argument with the length of the returned string.
+ *
+ * Returns: (transfer full): the string of the buffer
+ *   which must be freed by the caller. The @strbuf
+ *   is afterwards in undefined state, though it can be
+ *   reused after nm_str_buf_init(). */
+static inline char *
+nm_str_buf_finalize (NMStrBuf *strbuf,
+                     gsize *out_len)
+{
+	nm_str_buf_maybe_expand (strbuf, 1, TRUE);
+	strbuf->_priv_str[strbuf->_priv_len] = '\0';
+
+	NM_SET_OUT (out_len, strbuf->_priv_len);
+
+	/* the buffer is in invalid state afterwards, however, we clear it
+	 * so far, that nm_auto_str_buf and nm_str_buf_destroy() is happy.  */
+	return g_steal_pointer (&strbuf->_priv_str);
+}
+
+/**
+ * nm_str_buf_destroy:
+ * @strbuf: an initialized #NMStrBuf
+ *
+ * Frees the associated memory of @strbuf. The buffer
+ * afterwards is in undefined state, but can be re-initialized
+ * with nm_str_buf_init().
+ */
+static inline void
+nm_str_buf_destroy (NMStrBuf *strbuf)
+{
+	if (!strbuf->_priv_str)
+		return;
+	_nm_str_buf_assert (strbuf);
+	if (strbuf->_priv_do_bzero_mem)
+		nm_explicit_bzero (strbuf->_priv_str, strbuf->_priv_len);
+	g_free (strbuf->_priv_str);
+
+	/* the buffer is in invalid state afterwards, however, we clear it
+	 * so far, that nm_auto_str_buf is happy when calling
+	 * nm_str_buf_destroy() again.  */
+	strbuf->_priv_str = NULL;
+}
+
+#define nm_auto_str_buf    nm_auto (nm_str_buf_destroy)
+
+#endif /* __NM_STR_BUF_H__ */
diff --git a/shared/nm-glib-aux/nm-time-utils.c b/shared/nm-glib-aux/nm-time-utils.c
index 20d663bc..356ed1a5 100644
--- a/shared/nm-glib-aux/nm-time-utils.c
+++ b/shared/nm-glib-aux/nm-time-utils.c
@@ -40,19 +40,19 @@ _t_init_global_state (void)
 	/* The only failure we tolerate is that CLOCK_BOOTTIME is not supported.
 	 * Other than that, we rely on kernel to not fail on this. */
 	g_assert (r == 0);
-	g_assert (tp.tv_nsec >= 0 && tp.tv_nsec < NM_UTILS_NS_PER_SECOND);
+	g_assert (tp.tv_nsec >= 0 && tp.tv_nsec < NM_UTILS_NSEC_PER_SEC);
 
 	/* Calculate an offset for the time stamp.
 	 *
 	 * We always want positive values, because then we can initialize
 	 * a timestamp with 0 and be sure, that it will be less then any
 	 * value nm_utils_get_monotonic_timestamp_*() might return.
-	 * For this to be true also for nm_utils_get_monotonic_timestamp_s() at
+	 * For this to be true also for nm_utils_get_monotonic_timestamp_sec() at
 	 * early boot, we have to shift the timestamp to start counting at
 	 * least from 1 second onward.
 	 *
 	 * Another advantage of shifting is, that this way we make use of the whole 31 bit
-	 * range of signed int, before the time stamp for nm_utils_get_monotonic_timestamp_s()
+	 * range of signed int, before the time stamp for nm_utils_get_monotonic_timestamp_sec()
 	 * wraps (~68 years).
 	 **/
 	offset_sec = (- ((gint64) tp.tv_sec)) + 1;
@@ -96,7 +96,7 @@ _t_init_global_state (void)
 		_r = clock_gettime (_p2->clk_id, _tp); \
 		\
 		nm_assert (_r == 0); \
-		nm_assert (_tp->tv_nsec >= 0 && _tp->tv_nsec < NM_UTILS_NS_PER_SECOND); \
+		nm_assert (_tp->tv_nsec >= 0 && _tp->tv_nsec < NM_UTILS_NSEC_PER_SEC); \
 		\
 		_p2; \
 	})
@@ -107,7 +107,7 @@ _t_init_global_state (void)
 /*****************************************************************************/
 
 /**
- * nm_utils_get_monotonic_timestamp_ns:
+ * nm_utils_get_monotonic_timestamp_nsec:
  *
  * Returns: a monotonically increasing time stamp in nanoseconds,
  * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
@@ -115,11 +115,11 @@ _t_init_global_state (void)
  * The returned value will start counting at an undefined point
  * in the past and will always be positive.
  *
- * All the nm_utils_get_monotonic_timestamp_*s functions return the same
+ * All the nm_utils_get_monotonic_timestamp_*sec functions return the same
  * timestamp but in different scales (nsec, usec, msec, sec).
  **/
 gint64
-nm_utils_get_monotonic_timestamp_ns (void)
+nm_utils_get_monotonic_timestamp_nsec (void)
 {
 	const GlobalState *p;
 	struct timespec tp;
@@ -130,12 +130,12 @@ nm_utils_get_monotonic_timestamp_ns (void)
 	 * integer, which makes it easier to calculate time differences (when
 	 * you want to subtract signed values).
 	 **/
-	return (((gint64) tp.tv_sec) + p->offset_sec) * NM_UTILS_NS_PER_SECOND +
+	return (((gint64) tp.tv_sec) + p->offset_sec) * NM_UTILS_NSEC_PER_SEC +
 	       tp.tv_nsec;
 }
 
 /**
- * nm_utils_get_monotonic_timestamp_us:
+ * nm_utils_get_monotonic_timestamp_usec:
  *
  * Returns: a monotonically increasing time stamp in microseconds,
  * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
@@ -143,11 +143,11 @@ nm_utils_get_monotonic_timestamp_ns (void)
  * The returned value will start counting at an undefined point
  * in the past and will always be positive.
  *
- * All the nm_utils_get_monotonic_timestamp_*s functions return the same
+ * All the nm_utils_get_monotonic_timestamp_*sec functions return the same
  * timestamp but in different scales (nsec, usec, msec, sec).
  **/
 gint64
-nm_utils_get_monotonic_timestamp_us (void)
+nm_utils_get_monotonic_timestamp_usec (void)
 {
 	const GlobalState *p;
 	struct timespec tp;
@@ -159,11 +159,11 @@ nm_utils_get_monotonic_timestamp_us (void)
 	 * you want to subtract signed values).
 	 **/
 	return (((gint64) tp.tv_sec) + p->offset_sec) * ((gint64) G_USEC_PER_SEC) +
-	       (tp.tv_nsec / (NM_UTILS_NS_PER_SECOND/G_USEC_PER_SEC));
+	       (tp.tv_nsec / (NM_UTILS_NSEC_PER_SEC/G_USEC_PER_SEC));
 }
 
 /**
- * nm_utils_get_monotonic_timestamp_ms:
+ * nm_utils_get_monotonic_timestamp_msec:
  *
  * Returns: a monotonically increasing time stamp in milliseconds,
  * starting at an unspecified offset. See clock_gettime(), %CLOCK_BOOTTIME.
@@ -171,11 +171,11 @@ nm_utils_get_monotonic_timestamp_us (void)
  * The returned value will start counting at an undefined point
  * in the past and will always be positive.
  *
- * All the nm_utils_get_monotonic_timestamp_*s functions return the same
+ * All the nm_utils_get_monotonic_timestamp_*sec functions return the same
  * timestamp but in different scales (nsec, usec, msec, sec).
  **/
 gint64
-nm_utils_get_monotonic_timestamp_ms (void)
+nm_utils_get_monotonic_timestamp_msec (void)
 {
 	const GlobalState *p;
 	struct timespec tp;
@@ -187,23 +187,23 @@ nm_utils_get_monotonic_timestamp_ms (void)
 	 * you want to subtract signed values).
 	 **/
 	return (((gint64) tp.tv_sec) + p->offset_sec) * ((gint64) 1000) +
-	       (tp.tv_nsec / (NM_UTILS_NS_PER_SECOND/1000));
+	       (tp.tv_nsec / (NM_UTILS_NSEC_PER_SEC/1000));
 }
 
 /**
- * nm_utils_get_monotonic_timestamp_s:
+ * nm_utils_get_monotonic_timestamp_sec:
  *
- * Returns: nm_utils_get_monotonic_timestamp_ms() in seconds (throwing
+ * Returns: nm_utils_get_monotonic_timestamp_msec() in seconds (throwing
  * away sub second parts). The returned value will always be positive.
  *
  * This value wraps after roughly 68 years which should be fine for any
  * practical purpose.
  *
- * All the nm_utils_get_monotonic_timestamp_*s functions return the same
+ * All the nm_utils_get_monotonic_timestamp_*sec functions return the same
  * timestamp but in different scales (nsec, usec, msec, sec).
  **/
 gint32
-nm_utils_get_monotonic_timestamp_s (void)
+nm_utils_get_monotonic_timestamp_sec (void)
 {
 	const GlobalState *p;
 	struct timespec tp;
@@ -216,13 +216,13 @@ nm_utils_get_monotonic_timestamp_s (void)
 /**
  * nm_utils_monotonic_timestamp_as_boottime:
  * @timestamp: the monotonic-timestamp that should be converted into CLOCK_BOOTTIME.
- * @timestamp_ns_per_tick: How many nanoseconds make one unit of @timestamp? E.g. if
- *   @timestamp is in unit seconds, pass %NM_UTILS_NS_PER_SECOND; if @timestamp is
- *   in nanoseconds, pass 1; if @timestamp is in milliseconds, pass %NM_UTILS_NS_PER_SECOND/1000.
- *   This must be a multiple of 10, and between 1 and %NM_UTILS_NS_PER_SECOND.
+ * @timestamp_nsec_per_tick: How many nanoseconds make one unit of @timestamp? E.g. if
+ *   @timestamp is in unit seconds, pass %NM_UTILS_NSEC_PER_SEC; if @timestamp is
+ *   in nanoseconds, pass 1; if @timestamp is in milliseconds, pass %NM_UTILS_NSEC_PER_SEC/1000.
+ *   This must be a multiple of 10, and between 1 and %NM_UTILS_NSEC_PER_SEC.
  *
  * Returns: the monotonic-timestamp as CLOCK_BOOTTIME, as returned by clock_gettime().
- *   The unit is the same as the passed in @timestamp based on @timestamp_ns_per_tick.
+ *   The unit is the same as the passed in @timestamp based on @timestamp_nsec_per_tick.
  *   E.g. if you passed @timestamp in as seconds, it will return boottime in seconds.
  *
  *   Note that valid monotonic-timestamps are always positive numbers (counting roughly since
@@ -234,16 +234,16 @@ nm_utils_get_monotonic_timestamp_s (void)
  * On older kernels that don't support CLOCK_BOOTTIME, the returned time is instead CLOCK_MONOTONIC.
  **/
 gint64
-nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_per_tick)
+nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_nsec_per_tick)
 {
 	const GlobalState *p;
 	gint64 offset;
 
-	/* only support ns-per-tick being a multiple of 10. */
-	g_return_val_if_fail (timestamp_ns_per_tick == 1
-	                      || (timestamp_ns_per_tick > 0 &&
-	                          timestamp_ns_per_tick <= NM_UTILS_NS_PER_SECOND &&
-	                          timestamp_ns_per_tick % 10 == 0),
+	/* only support nsec-per-tick being a multiple of 10. */
+	g_return_val_if_fail (   timestamp_nsec_per_tick == 1
+	                      || (timestamp_nsec_per_tick > 0 &&
+	                          timestamp_nsec_per_tick <= NM_UTILS_NSEC_PER_SEC &&
+	                          timestamp_nsec_per_tick % 10 == 0),
 	                      -1);
 
 	/* if the caller didn't yet ever fetch a monotonic-timestamp, he cannot pass any meaningful
@@ -255,7 +255,7 @@ nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_
 	nm_assert (p->offset_sec <= 0);
 
 	/* calculate the offset of monotonic-timestamp to boottime. offset_s is <= 1. */
-	offset = p->offset_sec * (NM_UTILS_NS_PER_SECOND / timestamp_ns_per_tick);
+	offset = p->offset_sec * (NM_UTILS_NSEC_PER_SEC / timestamp_nsec_per_tick);
 
 	nm_assert (offset <= 0 && offset > G_MININT64);
 
@@ -270,9 +270,9 @@ nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_
  * @boottime: the timestamp from CLOCK_BOOTTIME (or CLOCK_MONOTONIC, if
  *   kernel does not support CLOCK_BOOTTIME and monotonic timestamps are based
  *   on CLOCK_MONOTONIC).
- * @timestamp_ns_per_tick: the scale in which @boottime is. If @boottime is in
+ * @timestamp_nsec_per_tick: the scale in which @boottime is. If @boottime is in
  *   nano seconds, this should be 1. If it is in milli seconds, this should be
- *   %NM_UTILS_NS_PER_SECOND/1000, etc.
+ *   %NM_UTILS_NSEC_PER_SEC/1000, etc.
  *
  * Returns: the same timestamp in monotonic timestamp scale.
  *
@@ -284,16 +284,16 @@ nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ns_
  * This is the inverse of nm_utils_monotonic_timestamp_as_boottime().
  */
 gint64
-nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_ns_per_tick)
+nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_nsec_per_tick)
 {
 	const GlobalState *p;
 	gint64 offset;
 
-	/* only support ns-per-tick being a multiple of 10. */
-	g_return_val_if_fail (timestamp_ns_per_tick == 1
-	                      || (timestamp_ns_per_tick > 0 &&
-	                          timestamp_ns_per_tick <= NM_UTILS_NS_PER_SECOND &&
-	                          timestamp_ns_per_tick % 10 == 0),
+	/* only support nsec-per-tick being a multiple of 10. */
+	g_return_val_if_fail (   timestamp_nsec_per_tick == 1
+	                      || (timestamp_nsec_per_tick > 0 &&
+	                          timestamp_nsec_per_tick <= NM_UTILS_NSEC_PER_SEC &&
+	                          timestamp_nsec_per_tick % 10 == 0),
 	                      -1);
 
 	p = _t_get_global_state ();
@@ -301,7 +301,7 @@ nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_n
 	nm_assert (p->offset_sec <= 0);
 
 	/* calculate the offset of monotonic-timestamp to boottime. offset_s is <= 1. */
-	offset = p->offset_sec * (NM_UTILS_NS_PER_SECOND / timestamp_ns_per_tick);
+	offset = p->offset_sec * (NM_UTILS_NSEC_PER_SEC / timestamp_nsec_per_tick);
 
 	nm_assert (offset <= 0 && offset > G_MININT64);
 
@@ -312,21 +312,21 @@ nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_n
 }
 
 gint64
-nm_utils_clock_gettime_ns (clockid_t clockid)
+nm_utils_clock_gettime_nsec (clockid_t clockid)
 {
 	struct timespec tp;
 
 	if (clock_gettime (clockid, &tp) != 0)
 		return -NM_ERRNO_NATIVE (errno);
-	return nm_utils_timespec_to_ns (&tp);
+	return nm_utils_timespec_to_nsec (&tp);
 }
 
 gint64
-nm_utils_clock_gettime_ms (clockid_t clockid)
+nm_utils_clock_gettime_msec (clockid_t clockid)
 {
 	struct timespec tp;
 
 	if (clock_gettime (clockid, &tp) != 0)
 		return -NM_ERRNO_NATIVE (errno);
-	return nm_utils_timespec_to_ms (&tp);
+	return nm_utils_timespec_to_msec (&tp);
 }
diff --git a/shared/nm-glib-aux/nm-time-utils.h b/shared/nm-glib-aux/nm-time-utils.h
index 8bf41b96..190d9491 100644
--- a/shared/nm-glib-aux/nm-time-utils.h
+++ b/shared/nm-glib-aux/nm-time-utils.h
@@ -9,34 +9,42 @@
 #include <time.h>
 
 static inline gint64
-nm_utils_timespec_to_ns (const struct timespec *ts)
+nm_utils_timespec_to_nsec (const struct timespec *ts)
 {
-	return   (((gint64) ts->tv_sec) * ((gint64) NM_UTILS_NS_PER_SECOND))
+	return   (((gint64) ts->tv_sec) * ((gint64) NM_UTILS_NSEC_PER_SEC))
 	       +  ((gint64) ts->tv_nsec);
 }
 
 static inline gint64
-nm_utils_timespec_to_ms (const struct timespec *ts)
+nm_utils_timespec_to_msec (const struct timespec *ts)
 {
 	return   (((gint64) ts->tv_sec)  * ((gint64) 1000))
-	       + (((gint64) ts->tv_nsec) / ((gint64) NM_UTILS_NS_PER_SECOND / 1000));
+	       + (((gint64) ts->tv_nsec) / ((gint64) NM_UTILS_NSEC_PER_SEC / 1000));
 }
 
-gint64 nm_utils_get_monotonic_timestamp_ns (void);
-gint64 nm_utils_get_monotonic_timestamp_us (void);
-gint64 nm_utils_get_monotonic_timestamp_ms (void);
-gint32 nm_utils_get_monotonic_timestamp_s (void);
-gint64 nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ticks_per_ns);
-gint64 nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_ns_per_tick);
+gint64 nm_utils_get_monotonic_timestamp_nsec (void);
+gint64 nm_utils_get_monotonic_timestamp_usec (void);
+gint64 nm_utils_get_monotonic_timestamp_msec (void);
+gint32 nm_utils_get_monotonic_timestamp_sec (void);
+
+gint64 nm_utils_monotonic_timestamp_as_boottime (gint64 timestamp, gint64 timestamp_ticks_per_nsec);
+gint64 nm_utils_monotonic_timestamp_from_boottime (guint64 boottime, gint64 timestamp_nsec_per_tick);
+
+static inline gint64
+nm_utils_get_monotonic_timestamp_nsec_cached (gint64 *cache_now)
+{
+	return    (*cache_now)
+	       ?: (*cache_now = nm_utils_get_monotonic_timestamp_nsec ());
+}
 
 static inline gint64
-nm_utils_get_monotonic_timestamp_ns_cached (gint64 *cache_now)
+nm_utils_get_monotonic_timestamp_msec_cached (gint64 *cache_now)
 {
 	return    (*cache_now)
-	       ?: (*cache_now = nm_utils_get_monotonic_timestamp_ns ());
+	       ?: (*cache_now = nm_utils_get_monotonic_timestamp_msec ());
 }
 
-gint64 nm_utils_clock_gettime_ns (clockid_t clockid);
-gint64 nm_utils_clock_gettime_ms (clockid_t clockid);
+gint64 nm_utils_clock_gettime_nsec (clockid_t clockid);
+gint64 nm_utils_clock_gettime_msec (clockid_t clockid);
 
 #endif /* __NM_TIME_UTILS_H__ */
diff --git a/shared/nm-glib-aux/tests/meson.build b/shared/nm-glib-aux/tests/meson.build
new file mode 100644
index 00000000..f6328db9
--- /dev/null
+++ b/shared/nm-glib-aux/tests/meson.build
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+test_unit = 'test-shared-general'
+
+c_flags = [
+  '-DNETWORKMANAGER_COMPILATION_TEST',
+  '-DNETWORKMANAGER_COMPILATION=(NM_NETWORKMANAGER_COMPILATION_GLIB|NM_NETWORKMANAGER_COMPILATION_WITH_GLIB_I18N_PROG)',
+]
+
+exe = executable(
+  test_unit,
+  test_unit + '.c',
+  c_args: c_flags,
+  dependencies: libnm_utils_base_dep,
+  link_with: libnm_systemd_logging_stub,
+)
+
+test(
+  'shared/nm-glib-aux/' + test_unit,
+  test_script,
+  args: test_args + [exe.full_path()],
+  timeout: default_test_timeout,
+)
diff --git a/shared/nm-glib-aux/tests/test-shared-general.c b/shared/nm-glib-aux/tests/test-shared-general.c
new file mode 100644
index 00000000..5c8bdf18
--- /dev/null
+++ b/shared/nm-glib-aux/tests/test-shared-general.c
@@ -0,0 +1,797 @@
+// SPDX-License-Identifier: LGPL-2.1+
+/*
+ * Copyright (C) 2018 Red Hat, Inc.
+ */
+
+#define NM_TEST_UTILS_NO_LIBNM 1
+
+#include "nm-default.h"
+
+#include "nm-std-aux/unaligned.h"
+#include "nm-glib-aux/nm-random-utils.h"
+#include "nm-glib-aux/nm-str-buf.h"
+#include "nm-glib-aux/nm-time-utils.h"
+#include "nm-glib-aux/nm-ref-string.h"
+
+#include "nm-utils/nm-test-utils.h"
+
+/*****************************************************************************/
+
+static void
+test_gpid (void)
+{
+	const int *int_ptr;
+	GPid pid = 42;
+
+	/* We redefine G_PID_FORMAT, because it's only available since glib 2.53.5.
+	 *
+	 * Also, this is the format for GPid, which for glib is always a typedef
+	 * for "int". Add a check for that here.
+	 *
+	 * G_PID_FORMAT is not about pid_t, which might be a smaller int, and which we would
+	 * check with SIZEOF_PID_T. */
+	G_STATIC_ASSERT (sizeof (GPid) == sizeof (int));
+
+	g_assert_cmpstr (""G_PID_FORMAT, ==, "i");
+
+	/* check that it's really "int". We will get a compiler warning, if that's not
+	 * the case. */
+	int_ptr = &pid;
+	g_assert_cmpint (*int_ptr, ==, 42);
+}
+
+/*****************************************************************************/
+
+static void
+test_monotonic_timestamp (void)
+{
+	g_assert (nm_utils_get_monotonic_timestamp_sec () > 0);
+}
+
+/*****************************************************************************/
+
+static void
+test_nmhash (void)
+{
+	int rnd;
+
+	nm_utils_random_bytes (&rnd, sizeof (rnd));
+
+	g_assert (nm_hash_val (555, 4) != 0);
+}
+
+/*****************************************************************************/
+
+static const char *
+_make_strv_foo (void)
+{
+	return "foo";
+}
+
+static const char *const*const _tst_make_strv_1 = NM_MAKE_STRV ("1", "2");
+
+static void
+test_make_strv (void)
+{
+	const char *const*v1a = NM_MAKE_STRV ("a");
+	const char *const*v1b = NM_MAKE_STRV ("a", );
+	const char *const*v2a = NM_MAKE_STRV ("a", "b");
+	const char *const*v2b = NM_MAKE_STRV ("a", "b", );
+	const char *const v3[] = { "a", "b", };
+	const char *const*v4b = NM_MAKE_STRV ("a", _make_strv_foo (), );
+
+	g_assert (NM_PTRARRAY_LEN (v1a) == 1);
+	g_assert (NM_PTRARRAY_LEN (v1b) == 1);
+	g_assert (NM_PTRARRAY_LEN (v2a) == 2);
+	g_assert (NM_PTRARRAY_LEN (v2b) == 2);
+
+	g_assert (NM_PTRARRAY_LEN (_tst_make_strv_1) == 2);
+	g_assert_cmpstr (_tst_make_strv_1[0], ==, "1");
+	g_assert_cmpstr (_tst_make_strv_1[1], ==, "2");
+	/* writing the static read-only variable leads to crash .*/
+	//((char **) _tst_make_strv_1)[0] = NULL;
+	//((char **) _tst_make_strv_1)[2] = "c";
+
+	G_STATIC_ASSERT_EXPR (G_N_ELEMENTS (v3) == 2);
+
+	g_assert (NM_PTRARRAY_LEN (v4b) == 2);
+
+	G_STATIC_ASSERT_EXPR (G_N_ELEMENTS (NM_MAKE_STRV ("a", "b"  )) == 3);
+	G_STATIC_ASSERT_EXPR (G_N_ELEMENTS (NM_MAKE_STRV ("a", "b", )) == 3);
+
+	nm_strquote_a (300, "");
+}
+
+/*****************************************************************************/
+
+typedef enum {
+	TEST_NM_STRDUP_ENUM_m1 = -1,
+	TEST_NM_STRDUP_ENUM_3  = 3,
+} TestNMStrdupIntEnum;
+
+static void
+test_nm_strdup_int (void)
+{
+#define _NM_STRDUP_INT_TEST(num, str) \
+	G_STMT_START { \
+		gs_free char *_s1 = NULL; \
+		\
+		_s1 = nm_strdup_int ((num)); \
+		\
+		g_assert (_s1); \
+		g_assert_cmpstr (_s1, ==, str); \
+	} G_STMT_END
+
+#define _NM_STRDUP_INT_TEST_TYPED(type, num) \
+	G_STMT_START { \
+		type _num = ((type) num); \
+		\
+		_NM_STRDUP_INT_TEST (_num, G_STRINGIFY (num)); \
+	} G_STMT_END
+
+	_NM_STRDUP_INT_TEST_TYPED (char, 0);
+	_NM_STRDUP_INT_TEST_TYPED (char, 1);
+	_NM_STRDUP_INT_TEST_TYPED (guint8, 0);
+	_NM_STRDUP_INT_TEST_TYPED (gint8, 25);
+	_NM_STRDUP_INT_TEST_TYPED (char, 47);
+	_NM_STRDUP_INT_TEST_TYPED (short, 47);
+	_NM_STRDUP_INT_TEST_TYPED (int, 47);
+	_NM_STRDUP_INT_TEST_TYPED (long, 47);
+	_NM_STRDUP_INT_TEST_TYPED (unsigned char, 47);
+	_NM_STRDUP_INT_TEST_TYPED (unsigned short, 47);
+	_NM_STRDUP_INT_TEST_TYPED (unsigned, 47);
+	_NM_STRDUP_INT_TEST_TYPED (unsigned long, 47);
+	_NM_STRDUP_INT_TEST_TYPED (gint64, 9223372036854775807);
+	_NM_STRDUP_INT_TEST_TYPED (gint64, -9223372036854775807);
+	_NM_STRDUP_INT_TEST_TYPED (guint64, 0);
+	_NM_STRDUP_INT_TEST_TYPED (guint64, 9223372036854775807);
+
+	_NM_STRDUP_INT_TEST (TEST_NM_STRDUP_ENUM_m1, "-1");
+	_NM_STRDUP_INT_TEST (TEST_NM_STRDUP_ENUM_3,  "3");
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_strndup_a (void)
+{
+	int run;
+
+	for (run = 0; run < 20; run++) {
+		gs_free char *input = NULL;
+		char ch;
+		gsize i, l;
+
+		input = g_strnfill (nmtst_get_rand_uint32 () % 20, 'x');
+
+		for (i = 0; input[i]; i++) {
+			while ((ch = ((char) nmtst_get_rand_uint32 ())) == '\0') {
+				/* repeat. */
+			}
+			input[i] = ch;
+		}
+
+		{
+			gs_free char *dup_free = NULL;
+			const char *dup;
+
+			l = strlen (input) + 1;
+			dup = nm_strndup_a (10, input, l - 1, &dup_free);
+			g_assert_cmpstr (dup, ==, input);
+			if (strlen (dup) < 10)
+				g_assert (!dup_free);
+			else
+				g_assert (dup == dup_free);
+		}
+
+		{
+			gs_free char *dup_free = NULL;
+			const char *dup;
+
+			l = nmtst_get_rand_uint32 () % 23;
+			dup = nm_strndup_a (10, input, l, &dup_free);
+			g_assert (strncmp (dup, input, l) == 0);
+			g_assert (strlen (dup) <= l);
+			if (l < 10)
+				g_assert (!dup_free);
+			else
+				g_assert (dup == dup_free);
+			if (strlen (input) < l)
+				g_assert (nm_utils_memeqzero (&dup[strlen (input)], l - strlen (input)));
+		}
+	}
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_ip4_addr_is_localhost (void)
+{
+	g_assert ( nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("127.0.0.0")));
+	g_assert ( nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("127.0.0.1")));
+	g_assert ( nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("127.5.0.1")));
+	g_assert (!nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("126.5.0.1")));
+	g_assert (!nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("128.5.0.1")));
+	g_assert (!nm_ip4_addr_is_localhost (nmtst_inet4_from_string ("129.5.0.1")));
+}
+
+/*****************************************************************************/
+
+static void
+test_unaligned (void)
+{
+	int shift;
+
+	for (shift = 0; shift <= 32; shift++) {
+		guint8 buf[100] = { };
+		guint8 val = 0;
+
+		while (val == 0)
+			val = nmtst_get_rand_uint32 () % 256;
+
+		buf[shift] = val;
+
+		g_assert_cmpint (unaligned_read_le64 (&buf[shift]), ==, (guint64) val);
+		g_assert_cmpint (unaligned_read_be64 (&buf[shift]), ==, ((guint64) val) << 56);
+		g_assert_cmpint (unaligned_read_ne64 (&buf[shift]), !=, 0);
+
+		g_assert_cmpint (unaligned_read_le32 (&buf[shift]), ==, (guint32) val);
+		g_assert_cmpint (unaligned_read_be32 (&buf[shift]), ==, ((guint32) val) << 24);
+		g_assert_cmpint (unaligned_read_ne32 (&buf[shift]), !=, 0);
+
+		g_assert_cmpint (unaligned_read_le16 (&buf[shift]), ==, (guint16) val);
+		g_assert_cmpint (unaligned_read_be16 (&buf[shift]), ==, ((guint16) val) << 8);
+		g_assert_cmpint (unaligned_read_ne16 (&buf[shift]), !=, 0);
+	}
+}
+
+/*****************************************************************************/
+
+static void
+_strv_cmp_fuzz_input (const char *const*in,
+                      gssize l,
+                      const char ***out_strv_free_shallow,
+                      char ***out_strv_free_deep,
+                      const char *const* *out_s1,
+                      const char *const* *out_s2)
+{
+	const char **strv;
+	gsize i;
+
+	/* Fuzz the input argument. It will return two output arrays that are semantically
+	 * equal the input. */
+
+	if (nmtst_get_rand_bool ()) {
+		char **ss;
+
+		if (l < 0)
+			ss = g_strdupv ((char **) in);
+		else if (l == 0) {
+			ss =   nmtst_get_rand_bool ()
+			     ? NULL
+			     : g_new0 (char *, 1);
+		} else {
+			ss = nm_memdup (in, sizeof (const char *) * l);
+			for (i = 0; i < (gsize) l; i++)
+				ss[i] = g_strdup (ss[i]);
+		}
+		strv = (const char **) ss;
+		*out_strv_free_deep = ss;
+	} else {
+		if (l < 0) {
+			strv =   in
+			       ? nm_memdup (in, sizeof (const char *) * (NM_PTRARRAY_LEN (in) + 1))
+			       : NULL;
+		} else if (l == 0) {
+			strv =   nmtst_get_rand_bool ()
+			       ? NULL
+			       : g_new0 (const char *, 1);
+		} else
+			strv = nm_memdup (in, sizeof (const char *) * l);
+		*out_strv_free_shallow = strv;
+	}
+
+	*out_s1 = in;
+	*out_s2 = strv;
+
+	if (nmtst_get_rand_bool ()) {
+		/* randomly swap the original and the clone. That means, out_s1 is either
+		 * the input argument (as-is) or the sementically equal clone. */
+		NM_SWAP (*out_s1, *out_s2);
+	}
+	if (nmtst_get_rand_bool ()) {
+		/* randomly make s1 and s2 the same. This is for testing that
+		 * comparing two identical pointers yields the same result. */
+		*out_s2 = *out_s1;
+	}
+}
+
+static void
+_strv_cmp_free_deep (char **strv,
+                     gssize len)
+{
+	gssize i;
+
+	if (strv) {
+		if (len < 0)
+			g_strfreev (strv);
+		else {
+			for (i = 0; i < len; i++)
+				g_free (strv[i]);
+			g_free (strv);
+		}
+	}
+}
+
+static void
+test_strv_cmp (void)
+{
+	const char *const strv0[1] = { };
+	const char *const strv1[2] = { "", };
+
+#define _STRV_CMP(a1, l1, a2, l2, equal) \
+	G_STMT_START { \
+		gssize _l1 = (l1); \
+		gssize _l2 = (l2); \
+		const char *const*_a1; \
+		const char *const*_a2; \
+		const char *const*_a1x; \
+		const char *const*_a2x; \
+		char **_a1_free_deep = NULL; \
+		char **_a2_free_deep = NULL; \
+		gs_free const char **_a1_free_shallow = NULL; \
+		gs_free const char **_a2_free_shallow = NULL; \
+		int _c1, _c2; \
+		\
+		_strv_cmp_fuzz_input ((a1), _l1, &_a1_free_shallow, &_a1_free_deep, &_a1, &_a1x); \
+		_strv_cmp_fuzz_input ((a2), _l2, &_a2_free_shallow, &_a2_free_deep, &_a2, &_a2x); \
+		\
+		_c1 = _nm_utils_strv_cmp_n (_a1, _l1, _a2, _l2); \
+		_c2 = _nm_utils_strv_cmp_n (_a2, _l2, _a1, _l1); \
+		if (equal) { \
+			g_assert_cmpint (_c1, ==, 0); \
+			g_assert_cmpint (_c2, ==, 0); \
+		} else { \
+			g_assert_cmpint (_c1, ==, -1); \
+			g_assert_cmpint (_c2, ==, 1); \
+		} \
+		\
+		/* Compare with self. _strv_cmp_fuzz_input() randomly swapped the arguments (_a1 and _a1x).
+		 * Either way, the arrays must compare equal to their semantically equal alternative. */ \
+		g_assert_cmpint (_nm_utils_strv_cmp_n (_a1, _l1, _a1x, _l1), ==, 0); \
+		g_assert_cmpint (_nm_utils_strv_cmp_n (_a2, _l2, _a2x, _l2), ==, 0); \
+		\
+		_strv_cmp_free_deep (_a1_free_deep, _l1); \
+		_strv_cmp_free_deep (_a2_free_deep, _l2); \
+	} G_STMT_END
+
+	_STRV_CMP (NULL,  -1, NULL,  -1, TRUE);
+
+	_STRV_CMP (NULL,  -1, NULL,   0, FALSE);
+	_STRV_CMP (NULL,  -1, strv0,  0, FALSE);
+	_STRV_CMP (NULL,  -1, strv0, -1, FALSE);
+
+	_STRV_CMP (NULL,   0, NULL,   0, TRUE);
+	_STRV_CMP (NULL,   0, strv0,  0, TRUE);
+	_STRV_CMP (NULL,   0, strv0, -1, TRUE);
+	_STRV_CMP (strv0,  0, strv0,  0, TRUE);
+	_STRV_CMP (strv0,  0, strv0, -1, TRUE);
+	_STRV_CMP (strv0, -1, strv0, -1, TRUE);
+
+	_STRV_CMP (NULL,   0, strv1, -1, FALSE);
+	_STRV_CMP (NULL,   0, strv1,  1, FALSE);
+	_STRV_CMP (strv0,  0, strv1, -1, FALSE);
+	_STRV_CMP (strv0,  0, strv1,  1, FALSE);
+	_STRV_CMP (strv0, -1, strv1, -1, FALSE);
+	_STRV_CMP (strv0, -1, strv1,  1, FALSE);
+
+	_STRV_CMP (strv1, -1, strv1,  1, TRUE);
+	_STRV_CMP (strv1,  1, strv1,  1, TRUE);
+}
+
+/*****************************************************************************/
+
+static void
+_do_strstrip_avoid_copy (const char *str)
+{
+	gs_free char *str1 = g_strdup (str);
+	gs_free char *str2 = g_strdup (str);
+	gs_free char *str3 = NULL;
+	gs_free char *str4 = NULL;
+	const char *s3;
+	const char *s4;
+
+	if (str1)
+		g_strstrip (str1);
+
+	nm_strstrip (str2);
+
+	g_assert_cmpstr (str1, ==, str2);
+
+	s3 = nm_strstrip_avoid_copy (str, &str3);
+	g_assert_cmpstr (str1, ==, s3);
+
+	s4 = nm_strstrip_avoid_copy_a (10, str, &str4);
+	g_assert_cmpstr (str1, ==, s4);
+	g_assert (!str == !s4);
+	g_assert (!s4 || strlen (s4) <= strlen (str));
+	if (s4 && s4 == &str[strlen (str) - strlen (s4)]) {
+		g_assert (!str4);
+		g_assert (s3 == s4);
+	} else if (s4 && strlen (s4) >= 10) {
+		g_assert (str4);
+		g_assert (s4 == str4);
+	} else
+		g_assert (!str4);
+
+	if (!nm_streq0 (str1, str))
+		_do_strstrip_avoid_copy (str1);
+}
+
+static void
+test_strstrip_avoid_copy (void)
+{
+	_do_strstrip_avoid_copy (NULL);
+	_do_strstrip_avoid_copy ("");
+	_do_strstrip_avoid_copy (" ");
+	_do_strstrip_avoid_copy (" a ");
+	_do_strstrip_avoid_copy (" 012345678 ");
+	_do_strstrip_avoid_copy (" 0123456789 ");
+	_do_strstrip_avoid_copy (" 01234567890 ");
+	_do_strstrip_avoid_copy (" 012345678901 ");
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_utils_bin2hexstr (void)
+{
+	int n_run;
+
+	for (n_run = 0; n_run < 100; n_run++) {
+		guint8 buf[100];
+		guint8 buf2[G_N_ELEMENTS (buf) + 1];
+		gsize len = nmtst_get_rand_uint32 () % (G_N_ELEMENTS (buf) + 1);
+		char strbuf1[G_N_ELEMENTS (buf) * 3];
+		gboolean allocate = nmtst_get_rand_bool ();
+		char delimiter = nmtst_get_rand_bool () ? ':' : '\0';
+		gboolean upper_case = nmtst_get_rand_bool ();
+		gsize expected_strlen;
+		char *str_hex;
+		gsize required_len;
+		gboolean outlen_set;
+		gsize outlen;
+		guint8 *bin2;
+
+		nmtst_rand_buf (NULL, buf, len);
+
+		if (len == 0)
+			expected_strlen = 0;
+		else if (delimiter != '\0')
+			expected_strlen = (len * 3u) - 1;
+		else
+			expected_strlen = len * 2u;
+
+		g_assert_cmpint (expected_strlen, <, G_N_ELEMENTS (strbuf1));
+
+		str_hex = nm_utils_bin2hexstr_full (buf, len, delimiter, upper_case, !allocate ? strbuf1 : NULL);
+
+		g_assert (str_hex);
+		if (!allocate)
+			g_assert (str_hex == strbuf1);
+		g_assert_cmpint (strlen (str_hex), ==, expected_strlen);
+
+		g_assert (NM_STRCHAR_ALL (str_hex, ch,    (ch >= '0' && ch <= '9')
+		                                       || ch == delimiter
+		                                       || (  upper_case
+		                                           ? (ch >= 'A' && ch <= 'F')
+		                                           : (ch >= 'a' && ch <= 'f'))));
+
+		required_len = nmtst_get_rand_bool () ? len : 0u;
+
+		outlen_set = required_len == 0 || nmtst_get_rand_bool ();
+
+		memset (buf2, 0, sizeof (buf2));
+
+		bin2 = nm_utils_hexstr2bin_full (str_hex,
+		                                 nmtst_get_rand_bool (),
+		                                 delimiter != '\0' && nmtst_get_rand_bool (),
+		                                   delimiter != '\0'
+		                                 ? nmtst_rand_select ((const char *) ":", ":-")
+		                                 : nmtst_rand_select ((const char *) ":", ":-", "", NULL),
+		                                 required_len,
+		                                 buf2,
+		                                 len,
+		                                 outlen_set ? &outlen : NULL);
+		if (len > 0) {
+			g_assert (bin2);
+			g_assert (bin2 == buf2);
+		} else
+			g_assert (!bin2);
+
+		if (outlen_set)
+			g_assert_cmpint (outlen, ==, len);
+
+		g_assert_cmpmem (buf, len, buf2, len);
+
+		g_assert (buf2[len] == '\0');
+
+		if (allocate)
+			g_free (str_hex);
+	}
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_ref_string (void)
+{
+	nm_auto_ref_string NMRefString *s1 = NULL;
+	NMRefString *s2;
+
+	s1 = nm_ref_string_new ("hallo");
+	g_assert (s1);
+	g_assert_cmpstr (s1->str, ==, "hallo");
+	g_assert_cmpint (s1->len, ==, strlen ("hallo"));
+
+	s2 = nm_ref_string_new ("hallo");
+	g_assert (s2 == s1);
+	nm_ref_string_unref (s2);
+
+	s2 = nm_ref_string_new (NULL);
+	g_assert (!s2);
+	nm_ref_string_unref (s2);
+
+#define STR_WITH_NUL "hallo\0test\0"
+	s2 = nm_ref_string_new_len (STR_WITH_NUL, NM_STRLEN (STR_WITH_NUL));
+	g_assert (s2);
+	g_assert_cmpstr (s2->str, ==, "hallo");
+	g_assert_cmpint (s2->len, ==, NM_STRLEN (STR_WITH_NUL));
+	g_assert_cmpint (s2->len, >, strlen (s2->str));
+	g_assert_cmpmem (s2->str, s2->len, STR_WITH_NUL, NM_STRLEN (STR_WITH_NUL));
+	g_assert (s2->str[s2->len] == '\0');
+	nm_ref_string_unref (s2);
+}
+
+/*****************************************************************************/
+
+static
+NM_UTILS_STRING_TABLE_LOOKUP_DEFINE (
+	_do_string_table_lookup,
+	int,
+	{ ; },
+	{ return -1; },
+	{ "0", 0 },
+	{ "1", 1 },
+	{ "2", 2 },
+	{ "3", 3 },
+)
+
+static void
+test_string_table_lookup (void)
+{
+	const char *const args[] = { NULL, "0", "1", "2", "3", "x", };
+	int i;
+
+	for (i = 0; i < G_N_ELEMENTS (args); i++) {
+		const char *needle = args[i];
+		const int val2 = _nm_utils_ascii_str_to_int64 (needle, 10, 0, 100, -1);
+		int val;
+
+		val = _do_string_table_lookup (needle);
+		g_assert_cmpint (val, ==, val2);
+	}
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_utils_get_next_realloc_size (void)
+{
+	static const struct {
+		gsize requested;
+		gsize reserved_true;
+		gsize reserved_false;
+	} test_data[] = {
+		{      0,     8,     8 },
+		{      1,     8,     8 },
+		{      8,     8,     8 },
+		{      9,    16,    16 },
+		{     16,    16,    16 },
+		{     17,    32,    32 },
+		{     32,    32,    32 },
+		{     33,    40,    40 },
+		{     40,    40,    40 },
+		{     41,   104,   104 },
+		{    104,   104,   104 },
+		{    105,   232,   232 },
+		{    232,   232,   232 },
+		{    233,   488,   488 },
+		{    488,   488,   488 },
+		{    489,  1000,  1000 },
+		{   1000,  1000,  1000 },
+		{   1001,  2024,  2024 },
+		{   2024,  2024,  2024 },
+		{   2025,  4072,  4072 },
+		{   4072,  4072,  4072 },
+		{   4073,  8168,  8168 },
+		{   8168,  8168,  8168 },
+		{   8169, 12264, 16360 },
+		{  12263, 12264, 16360 },
+		{  12264, 12264, 16360 },
+		{  12265, 16360, 16360 },
+		{  16360, 16360, 16360 },
+		{  16361, 20456, 32744 },
+		{  20456, 20456, 32744 },
+		{  20457, 24552, 32744 },
+		{  24552, 24552, 32744 },
+		{  24553, 28648, 32744 },
+		{  28648, 28648, 32744 },
+		{  28649, 32744, 32744 },
+		{  32744, 32744, 32744 },
+		{  32745, 36840, 65512 },
+		{  36840, 36840, 65512 },
+		{  G_MAXSIZE - 0x1000u,  G_MAXSIZE, G_MAXSIZE },
+		{  G_MAXSIZE - 25u,      G_MAXSIZE, G_MAXSIZE },
+		{  G_MAXSIZE - 24u,      G_MAXSIZE, G_MAXSIZE },
+		{  G_MAXSIZE - 1u,       G_MAXSIZE, G_MAXSIZE },
+		{  G_MAXSIZE,            G_MAXSIZE, G_MAXSIZE },
+		{  NM_UTILS_GET_NEXT_REALLOC_SIZE_104,  NM_UTILS_GET_NEXT_REALLOC_SIZE_104,  NM_UTILS_GET_NEXT_REALLOC_SIZE_104 },
+		{  NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000, NM_UTILS_GET_NEXT_REALLOC_SIZE_1000 },
+	};
+	guint i;
+
+	G_STATIC_ASSERT_EXPR (NM_UTILS_GET_NEXT_REALLOC_SIZE_104  == 104u);
+	G_STATIC_ASSERT_EXPR (NM_UTILS_GET_NEXT_REALLOC_SIZE_1000 == 1000u);
+
+	for (i = 0; i < G_N_ELEMENTS (test_data) + 5000u; i++) {
+		gsize requested0;
+
+		if (i < G_N_ELEMENTS (test_data))
+			requested0 = test_data[i].requested;
+		else {
+			/* find some interesting random values for testing. */
+			switch (nmtst_get_rand_uint32 () % 5) {
+			case 0:
+				requested0 = nmtst_get_rand_size ();
+				break;
+			case 1:
+				/* values close to G_MAXSIZE. */
+				requested0 = G_MAXSIZE - (nmtst_get_rand_uint32 () % 12000u);
+				break;
+			case 2:
+				/* values around G_MAXSIZE/2. */
+				requested0 = (G_MAXSIZE / 2u) + 6000u - (nmtst_get_rand_uint32 () % 12000u);
+				break;
+			case 3:
+				/* values around powers of 2. */
+				requested0 = (((gsize) 1) << (nmtst_get_rand_uint32 () % (sizeof (gsize) * 8u))) + 6000u - (nmtst_get_rand_uint32 () % 12000u);
+				break;
+			case 4:
+				/* values around 4k borders. */
+				requested0 = (nmtst_get_rand_size () & ~((gsize) 0xFFFu)) + 30u - (nmtst_get_rand_uint32 () % 60u);
+				break;
+			default: g_assert_not_reached ();
+			}
+		}
+
+		{
+			const gsize requested = requested0;
+			const gsize reserved_true = nm_utils_get_next_realloc_size (TRUE, requested);
+			const gsize reserved_false = nm_utils_get_next_realloc_size (FALSE, requested);
+
+			g_assert_cmpuint (reserved_true, >, 0);
+			g_assert_cmpuint (reserved_false, >, 0);
+			g_assert_cmpuint (reserved_true, >=, requested);
+			g_assert_cmpuint (reserved_false, >=, requested);
+			g_assert_cmpuint (reserved_false, >=, reserved_true);
+
+			if (i < G_N_ELEMENTS (test_data)) {
+				g_assert_cmpuint (reserved_true, ==, test_data[i].reserved_true);
+				g_assert_cmpuint (reserved_false, ==, test_data[i].reserved_false);
+			}
+
+			/* reserved_false is generally the next power of two - 24. */
+			if (reserved_false == G_MAXSIZE)
+				g_assert_cmpuint (requested, >, G_MAXSIZE / 2u - 24u);
+			else {
+				g_assert_cmpuint (reserved_false, <=, G_MAXSIZE - 24u);
+				if (reserved_false >= 40) {
+					const gsize _pow2 = reserved_false + 24u;
+
+					/* reserved_false must always be a power of two minus 24. */
+					g_assert_cmpuint (_pow2, >=, 64u);
+					g_assert_cmpuint (_pow2, >, requested);
+					g_assert (nm_utils_is_power_of_two (_pow2));
+
+					/* but _pow2/2 must also be smaller than what we requested. */
+					g_assert_cmpuint (_pow2 / 2u - 24u, <, requested);
+				} else {
+					/* smaller values are hard-coded. */
+				}
+			}
+
+			/* reserved_true is generally the next 4k border - 24. */
+			if (reserved_true == G_MAXSIZE)
+				g_assert_cmpuint (requested, >, G_MAXSIZE - 0x1000u - 24u);
+			else {
+				g_assert_cmpuint (reserved_true, <=, G_MAXSIZE - 24u);
+				if (reserved_true > 8168u) {
+					const gsize page_border = reserved_true + 24u;
+
+					/* reserved_true must always be aligned to 4k (minus 24). */
+					g_assert_cmpuint (page_border % 0x1000u, ==, 0);
+					if (requested > 0x1000u - 24u) {
+						/* page_border not be more than 4k above requested. */
+						g_assert_cmpuint (page_border, >=, 0x1000u - 24u);
+						g_assert_cmpuint (page_border - 0x1000u - 24u, <, requested);
+					}
+				} else {
+					/* for smaller sizes, reserved_true and reserved_false are the same. */
+					g_assert_cmpuint (reserved_true, ==, reserved_false);
+				}
+			}
+
+		}
+	}
+}
+
+/*****************************************************************************/
+
+static void
+test_nm_str_buf (void)
+{
+	guint i_run;
+
+	for (i_run = 0; TRUE; i_run++) {
+		nm_auto_str_buf NMStrBuf strbuf = { };
+		nm_auto_free_gstring GString *gstr = NULL;
+		int i, j, k;
+		int c;
+
+		nm_str_buf_init (&strbuf,
+		                 nmtst_get_rand_uint32 () % 200u + 1u,
+		                 nmtst_get_rand_bool ());
+
+		if (i_run < 1000) {
+			c = nmtst_get_rand_word_length (NULL);
+			for (i = 0; i < c; i++)
+				nm_str_buf_append_c (&strbuf, '0' + (i % 10));
+			gstr = g_string_new (nm_str_buf_get_str (&strbuf));
+			j = nmtst_get_rand_uint32 () % (strbuf.len + 1);
+			k = nmtst_get_rand_uint32 () % (strbuf.len - j + 2) - 1;
+
+			nm_str_buf_erase (&strbuf, j, k, nmtst_get_rand_bool ());
+			g_string_erase (gstr, j, k);
+			g_assert_cmpstr (gstr->str, ==, nm_str_buf_get_str (&strbuf));
+		} else
+			return;
+	}
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE ();
+
+int main (int argc, char **argv)
+{
+	nmtst_init (&argc, &argv, TRUE);
+
+	g_test_add_func ("/general/test_gpid", test_gpid);
+	g_test_add_func ("/general/test_monotonic_timestamp", test_monotonic_timestamp);
+	g_test_add_func ("/general/test_nmhash", test_nmhash);
+	g_test_add_func ("/general/test_nm_make_strv", test_make_strv);
+	g_test_add_func ("/general/test_nm_strdup_int", test_nm_strdup_int);
+	g_test_add_func ("/general/test_nm_strndup_a", test_nm_strndup_a);
+	g_test_add_func ("/general/test_nm_ip4_addr_is_localhost", test_nm_ip4_addr_is_localhost);
+	g_test_add_func ("/general/test_unaligned", test_unaligned);
+	g_test_add_func ("/general/test_strv_cmp", test_strv_cmp);
+	g_test_add_func ("/general/test_strstrip_avoid_copy", test_strstrip_avoid_copy);
+	g_test_add_func ("/general/test_nm_utils_bin2hexstr", test_nm_utils_bin2hexstr);
+	g_test_add_func ("/general/test_nm_ref_string", test_nm_ref_string);
+	g_test_add_func ("/general/test_string_table_lookup", test_string_table_lookup);
+	g_test_add_func ("/general/test_nm_utils_get_next_realloc_size", test_nm_utils_get_next_realloc_size);
+	g_test_add_func ("/general/test_nm_str_buf", test_nm_str_buf);
+
+	return g_test_run ();
+}